Skip to content

fix: cache file permission window, export-env quoting, shell guardrail gap - #22

Merged
DustyStudy merged 3 commits into
mainfrom
fix/cache-perms-export-quoting-shell-guardrails
Sep 22, 2026
Merged

DustyStudy merged 3 commits into
mainfrom
fix/cache-perms-export-quoting-shell-guardrails

Conversation

@DustyStudy

Copy link
Copy Markdown
Owner

Summary

Follow-up to #18/#20: the remaining lower-priority items from the original review — cache file permissions, export-env quoting, a real guardrail gap on shell, and the doc claims that didn't match. One commit per fix, each with regression tests checked against the pre-fix code.

  1. Cache files were briefly world-readable before chmod locked them down. put() wrote with Path.write_text() (mode from the process umask) then chmod'd afterward — a real window on POSIX systems during which a brand-new token/credentials file was readable by anyone. Now created via os.open() with an explicit 0o600 mode, set atomically. Still best-effort on Windows, same as before.
  2. export-env's output wasn't quoted. Credential values were interpolated straight into export KEY="value" / $env:KEY = "value". AWS credentials never actually contain shell metacharacters, but the values come from an external API response, so the interpolation itself shouldn't be a foot-gun for whatever evals it. POSIX lines now go through shlex.quote(); PowerShell lines escape `/"/$.
  3. protected_account_ids didn't actually cover shell, despite guardrails.yaml documenting it as blocking "ANY command via orgctl exec/shell" — spawn_shell() never called into guardrails at all. Pulled the protected-account check out of check_command() into its own check_protected_account() and wired it into spawn_shell() too. export-env/creds-process stay intentionally uncovered (documented in the example config) since they're meant to work non-interactively.
  4. Docs corrected. README's "Security model" and the threat model's asset table both claimed credentials are never exported outside the one child process, full stop — but export-env/creds-process print them to stdout by design. Scoped the claim to exec/shell and described what those two commands actually guard instead.

Verification

  • ruff check, ruff format --check, mypy src: clean.
  • pytest: 116 passed, 5 skipped (POSIX-only checks that don't apply on the Windows dev box — they run for real on the ubuntu-latest CI runners); coverage 70% → 73%.
  • Every regression test was checked against the pre-fix code (via git stash per file) and confirmed to fail there for the right reason, then pass with the fix. New tests/test_export_env.py includes an end-to-end test that actually evals the generated POSIX line in a real shell with a value containing shell metacharacters, and checks the value comes back unmangled rather than executed.

Not in this PR

Two intentionally-out-of-scope items surfaced along the way and documented rather than changed: export-env/creds-process don't get blocked by protected_account_ids (by design — see commit 3), and orgctl still has no signature/integrity check on orgs.yaml/guardrails.yaml themselves (pre-existing, named in the threat model's "explicitly out of scope" section).

put() wrote a new token/credentials file with Path.write_text() (mode
dictated by the process umask, often world-readable) and only restricted
access with chmod() afterward — a real window during which a brand-new
cache file was readable by anyone on POSIX systems. Create the file with
os.open() and an explicit 0o600 mode instead, which sets the permissions
atomically at creation time. Best-effort as before on Windows, where the
mode argument doesn't map onto real ACLs.
… shell

- export_env_lines() interpolated credential values straight into
  `export KEY="value"` / `$env:KEY = "value"` with no quoting. AWS
  credentials never actually contain shell metacharacters, but the values
  still come from an external API response, so the interpolation itself
  shouldn't be a foot-gun for whatever eval's the output. POSIX lines now
  go through shlex.quote(); PowerShell lines escape backtick/"/$.
- guardrails.yaml documents protected_account_ids as blocking "ANY command
  via `orgctl exec`/`shell`", but spawn_shell() never called into
  guardrails at all. There's no single "command" to pattern-match against
  for an interactive session, so pull the protected-account check out of
  check_command() into its own check_protected_account() and call that
  from spawn_shell() too. export-env/creds-process remain intentionally
  uncovered (see the updated comment in guardrails.example.yaml) since
  they're meant to work non-interactively.
README's "Security model" and THREAT_MODEL's asset table both said
credentials are never exported outside the one child process/shell that
requested them, full stop — but export-env and creds-process print
credentials to stdout by design (for eval into the current shell, or for
AWS tooling's credential_process protocol). Scope the claim to exec/shell
and describe what export-env/creds-process actually guard instead
(quoting, and stdout/stderr separation) — see the exec_cmd fix in this
same PR for the quoting itself.
@DustyStudy
DustyStudy merged commit ee94b9d into main Sep 22, 2026
9 checks passed
@DustyStudy
DustyStudy deleted the fix/cache-perms-export-quoting-shell-guardrails branch September 22, 2026 03:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant