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.rs — add_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).
Summary
On Windows,
compute_allow_paths_for_permissionsgates both the allow and the deny path insertion onif p.exists(). A protected read-only subpath (.git/.codex/.agents) that does not exist atsession 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.tomlis read astrusted Codex configuration,
.git/hooks/*is executed by git.Where
codex-rs/windows-sandbox-rs/src/allow.rs—add_deny_pathno-ops when the path does not exist:So a missing
.codex/.git/.agentsunder a writable root gets no deny, while the writable-rootallow covers it. Present on current
main.Reproduction
Faithful replication of the function (the
if p.exists()gate + the writable-root/read-only-subpathloop), stand-in
read_only_subpaths = [.git, .codex, .agents]: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_subpathunconditionally (deny a not-yet-existingpath 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).