diff --git a/codex-rs/windows-sandbox-rs/src/audit.rs b/codex-rs/windows-sandbox-rs/src/audit.rs index 9e02f86c1..659ea57b6 100644 --- a/codex-rs/windows-sandbox-rs/src/audit.rs +++ b/codex-rs/windows-sandbox-rs/src/audit.rs @@ -30,7 +30,7 @@ const SKIP_DIR_SUFFIXES: &[&str] = &[ "/programdata", ]; -fn normalize_path_key(p: &Path) -> String { +pub(crate) fn normalize_path_key(p: &Path) -> String { let n = dunce::canonicalize(p).unwrap_or_else(|_| p.to_path_buf()); n.to_string_lossy().replace('\\', "/").to_ascii_lowercase() } diff --git a/codex-rs/windows-sandbox-rs/src/identity.rs b/codex-rs/windows-sandbox-rs/src/identity.rs index 4418b0b07..835acc5d8 100644 --- a/codex-rs/windows-sandbox-rs/src/identity.rs +++ b/codex-rs/windows-sandbox-rs/src/identity.rs @@ -119,9 +119,10 @@ pub fn require_logon_sandbox_creds( ) -> Result { let sandbox_dir = crate::setup::sandbox_dir(codex_home); let needed_read = gather_read_roots(command_cwd, policy); - let mut needed_write = gather_write_roots(policy, policy_cwd, command_cwd, env_map); - // Ensure the sandbox directory itself is writable by sandbox users. - needed_write.push(sandbox_dir.clone()); + let needed_write = gather_write_roots(policy, policy_cwd, command_cwd, env_map); + // NOTE: Do not add CODEX_HOME/.sandbox to `needed_write`; it must remain non-writable by the + // restricted capability token. The setup helper's `lock_sandbox_dir` is responsible for + // granting the sandbox group access to this directory without granting the capability SID. let mut setup_reason: Option = None; let mut _existing_marker: Option = None; diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index 97ff1f0a8..c46e43349 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -408,15 +408,12 @@ fn build_payload_roots( read_roots_override: Option>, write_roots_override: Option>, ) -> (Vec, Vec) { - let sbx_dir = sandbox_dir(codex_home); - let mut write_roots = if let Some(roots) = write_roots_override { + let write_roots = if let Some(roots) = write_roots_override { canonical_existing(&roots) } else { gather_write_roots(policy, policy_cwd, command_cwd, env_map) }; - if !write_roots.contains(&sbx_dir) { - write_roots.push(sbx_dir.clone()); - } + let write_roots = filter_sensitive_write_roots(write_roots, codex_home); let mut read_roots = if let Some(roots) = read_roots_override { canonical_existing(&roots) } else { @@ -426,3 +423,17 @@ fn build_payload_roots( read_roots.retain(|root| !write_root_set.contains(root)); (read_roots, write_roots) } + +fn filter_sensitive_write_roots(mut roots: Vec, codex_home: &Path) -> Vec { + // Never grant capability write access to CODEX_HOME or anything under CODEX_HOME/.sandbox. + // These locations contain sandbox control/state and must remain tamper-resistant. + let codex_home_key = crate::audit::normalize_path_key(codex_home); + let sbx_dir_key = crate::audit::normalize_path_key(&sandbox_dir(codex_home)); + let sbx_dir_prefix = format!("{}/", sbx_dir_key.trim_end_matches('/')); + + roots.retain(|root| { + let key = crate::audit::normalize_path_key(root); + key != codex_home_key && key != sbx_dir_key && !key.starts_with(&sbx_dir_prefix) + }); + roots +}