Skip to content

fs sandbox resolve_under is purely lexical — symlinks inside the root escape JARVIS_FS_ROOT via ungated fs.read/code.grep #47

@TYRMars

Description

@TYRMars

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions