Skip to content

History directory and append-only writes bypass the package's own file/directory hardening #210

Description

@codeforester

Summary

write_history_record() and append_history_line() bypass the directory- and symlink-hardening that every other private-file write path in this package (_private_files.write_private_json) already applies, so the history directory ends up world-readable and the append-only write is not protected against a planted symlink.

Details

  • lib/python/base_cli/history.py:167 (write_history_record) creates its target directory with a plain path.parent.mkdir(parents=True, exist_ok=True). Unlike every runtime directory created via _runtime.create_runtime_directory/_private_files.restrict_directory (chmod 0700), this call never restricts permissions — it inherits the process umask. Reproduced directly: with a default umask of 0022, calling write_history_record() on a fresh path leaves the whole directory chain (e.g. cache_root/<app>/history/) at 0755, even though the file written inside it (runs.jsonl) is correctly chmod'd 0600 immediately after.
  • lib/python/base_cli/history.py:207-235 (append_history_line) opens the destination with os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND | binary_flag, 0o600) — no O_NOFOLLOW, no dir-fd anchoring. _private_files.write_private_json (used for every other private runtime file in the package) goes to considerable lengths to avoid exactly this class of issue: it opens the parent directory component-by-component with O_NOFOLLOW/dir_fd (_open_parent_directory), and explicitly refuses to replace a destination that is already a symlink. append_history_line has none of that, so if the target directory is left world-writable (or was created by an earlier, unpatched version of this same function per the point above) a local attacker could plant a symlink at the history path and have this writer follow it.

Impact

The persisted command-history file (runs.jsonl) is one of the more sensitive artifacts this package writes — it aggregates redacted argv and metadata across every invocation. Leaving its containing directory world-listable exposes run IDs, timestamps, and directory structure to other local users even though the file content itself is protected, and the missing symlink guard is a real (if narrower) gap against the package's own established hardening standard elsewhere in the same module (_private_files.py).

Suggested fix

Route write_history_record's directory creation through _private_files.restrict_directory (or _runtime.create_runtime_directory) instead of a raw mkdir, and add os.O_NOFOLLOW (where available) to append_history_line's open flags, consistent with the rest of the package's file-write hardening.

Metadata

Metadata

Assignees

Labels

securitySecurity hardening or vulnerability work

Type

No type

Projects

Status
Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions