Summary
The path sandbox resolve_under is purely lexical: it rejects absolute paths and .. components but never canonicalizes the result or checks for symlinks. A symlink that lives inside the sandbox root but points outside it is followed transparently, escaping JARVIS_FS_ROOT. The read-side tools that use it (fs.read, fs.list, code.grep) are not approval-gated, so this is a no-prompt host-file exfiltration path.
Details
crates/harness-tools/src/sandbox.rs:12-29:
pub(crate) fn resolve_under(root: &Path, rel: &str) -> Result<PathBuf, BoxError> {
let p = Path::new(rel);
if p.is_absolute() { return Err(...); }
for comp in p.components() {
match comp {
Component::ParentDir => return Err(...),
Component::Prefix(_) | Component::RootDir => return Err(...),
_ => {}
}
}
Ok(root.join(p)) // no canonicalize, no symlink check
}
If the workspace contains (or an enabled fs.write/shell.exec creates) a symlink link -> /etc, then fs.read {path:"link/passwd"} or code.grep over link/ resolves to /etc/passwd — outside the root. The write-side tools (fs.write/fs.edit/fs.patch) are approval-gated, but fs.read / fs.list / code.grep are not, so an attacker-planted symlink in any repo the agent merely reads (e.g. a malicious dependency checkout) lets the model exfiltrate arbitrary host files with no approval prompt.
Note: other tools in the crate (shell.rs, workspace.rs, doc.rs, todo.rs) do canonicalize their roots — only the shared path sandbox used by fs.* / grep / patch does not.
Impact
Sandbox escape for read tools with no approval gate; read of arbitrary host files. Severity: medium-high.
Suggested fix
After root.join(p), canonicalize the result (or canonicalize a pre-existing prefix and re-append the tail for not-yet-created paths) and verify it is still starts_with(canonical_root). Reject symlinks that resolve outside the root, matching what shell.rs/workspace.rs already do.
Summary
The path sandbox
resolve_underis purely lexical: it rejects absolute paths and..components but never canonicalizes the result or checks for symlinks. A symlink that lives inside the sandbox root but points outside it is followed transparently, escapingJARVIS_FS_ROOT. The read-side tools that use it (fs.read,fs.list,code.grep) are not approval-gated, so this is a no-prompt host-file exfiltration path.Details
crates/harness-tools/src/sandbox.rs:12-29:If the workspace contains (or an enabled
fs.write/shell.execcreates) a symlinklink -> /etc, thenfs.read {path:"link/passwd"}orcode.grepoverlink/resolves to/etc/passwd— outside the root. The write-side tools (fs.write/fs.edit/fs.patch) are approval-gated, butfs.read/fs.list/code.grepare not, so an attacker-planted symlink in any repo the agent merely reads (e.g. a malicious dependency checkout) lets the model exfiltrate arbitrary host files with no approval prompt.Note: other tools in the crate (
shell.rs,workspace.rs,doc.rs,todo.rs) do canonicalize their roots — only the shared path sandbox used byfs.*/grep/patchdoes not.Impact
Sandbox escape for read tools with no approval gate; read of arbitrary host files. Severity: medium-high.
Suggested fix
After
root.join(p), canonicalize the result (or canonicalize a pre-existing prefix and re-append the tail for not-yet-created paths) and verify it is stillstarts_with(canonical_root). Reject symlinks that resolve outside the root, matching whatshell.rs/workspace.rsalready do.