[RIP-SEC] Windows sandbox: a not-yet-existing protected dir (`.codex`/`.git`/`.agents`) gets no deny rule, so a sandboxed agent can create and poison it

Open 💬 0 comments Opened Aug 5, 2026 by scadastrangelove

Summary

On Windows, compute_allow_paths_for_permissions gates both the allow and the deny path insertion on
if p.exists(). A protected read-only subpath (.git/.codex/.agents) that does not exist at
session start therefore gets no deny ACE, while its parent writable root is allowed — so a
sandboxed agent can create the directory and write inside it (e.g. .codex/config.toml,
.git/hooks/pre-commit). A later, more-trusted step then consumes it: .codex/config.toml is read as
trusted Codex configuration, .git/hooks/* is executed by git.

Where

codex-rs/windows-sandbox-rs/src/allow.rsadd_deny_path no-ops when the path does not exist:

let mut add_deny_path = |p: PathBuf| { if p.exists() { deny.insert(p); } };

So a missing .codex/.git/.agents under a writable root gets no deny, while the writable-root
allow covers it. Present on current main.

Reproduction

Faithful replication of the function (the if p.exists() gate + the writable-root/read-only-subpath
loop), stand-in read_only_subpaths = [.git, .codex, .agents]:

BEFORE: allow=1 deny=0  -> .codex WRITABLE (bug)   # .codex absent → no deny, cwd allowed
AFTER : deny contains .codex -> control holds       # .codex created → now denied

The gate is purely existence-based. The crate's own test skips_protected_subdirs_when_missing
(windows-sandbox-rs/src/allow.rs) already documents the empty-deny-when-absent behavior.

Impact

On Windows, a sandboxed agent can create and poison a protected metadata directory that did not exist
at session start; the planted config/hook is executed/loaded by a later run. Deferred/conditional on
that later consumption, not immediate RCE.

Note on intent

The empty-deny-when-absent behavior is codified by the existing test above, so it may be considered
by-design; the angle here is the create-then-poison persistence consequence, which is not
documented. Filing at low severity accordingly.

Suggested fix

Emit the deny ACE for every protected read_only_subpath unconditionally (deny a not-yet-existing
path so its creation is refused), or deny the parent's protected-name pattern so first-time creation
is blocked — mirroring the Seatbelt backend, which excludes both the literal path and its subpath.

---
Found with the rust-in-peace pipeline
(AI-assisted Rust vulnerability research).

View original on GitHub ↗