fix(security): enforce --allowed-paths sandbox on file read tools#2344
Conversation
read_file in FileSearchToolsMixin called open() directly with no allowlist check, while write_file/edit_file enforce PathValidator. The --allowed-paths sandbox (documented as a security boundary) therefore only restricted writes: an agent could read any file the process owner could access — credentials, SSH keys, .env — regardless of the configured sandbox. The sibling FileIOToolsMixin already gates its reads, so reads were always meant to be confined. Gate every content-reading tool in the mixin (read_file, search_file_content, get_file_info, analyze_data_file) through a shared _read_access_error helper that mirrors the write-side enforcement. The check no-ops when no validator is attached, preserving behavior for hosts without a sandbox. The researcher's PoC (external report, CWE-862) no longer reproduces: read of a file outside allowed_paths now returns "Access denied" and no content reaches the caller.
|
Verdict: Approve This closes a real confidentiality hole: the file read tools ( One non-blocking observation for a follow-up (not this PR): 🔍 Technical detailsVerified
🟢 Minor — residual symlink read in Strengths
|
…or dicts Review follow-ups: - Force _is_interactive() False in the sandbox test fixture so out-of-sandbox reads auto-deny deterministically. Without it, `pytest -s` on a real TTY would hit the validator's input() prompt and hang. - Include has_errors on the access-denied dicts returned by get_file_info and analyze_data_file, matching every other error path in those two tools.
|
🟡 In No content leaks, but the fix inconsistency means the oracle protection it documents is only partial. Suggested fix for 🔍 Technical details
directory = Path(directory).resolve()
if not directory.exists(): # ← oracle: leaks existence for out-of-sandbox paths
return {"status": "error", "error": f"Directory not found: {directory}"}
denied = self._read_access_error(str(directory)) # ← sandbox check too lateFix: check the sandbox on the raw argument before resolving/probing, or at minimum before the existence check: denied = self._read_access_error(directory) # raw arg, before resolve
if denied:
return denied
directory = Path(directory).resolve()
if not directory.exists():
...
|
The sandbox check ran after the directory.exists() probe, so an out-of-sandbox path returned "Directory not found" vs "Access denied" — leaking whether an arbitrary directory exists. Move the allowlist check ahead of resolve()/exists() so it mirrors read_file and get_file_info, which already gate before the existence probe. Adds a regression test asserting a nonexistent out-of-sandbox dir returns access-denied.
|
Thanks for the review. Addressed in 2c23ec4:
Unrelated CI note: the only red check is |
|
🟡 In 🔍 Technical details
For
The caller (LLM) learns: "a file exists at Fix: swap the extension check and the sandbox check so the sandbox runs first: # Enforce sandbox on the resolved path (after indexed-file fallback, before extension check)
denied = self._read_access_error(str(fp))
if denied:
denied.update({"has_errors": True, "operation": "analyze_data_file"})
return denied
supported_extensions = {".csv", ".tsv", ".xlsx", ".xls"}
if fp.suffix.lower() not in supported_extensions:
...The comment's rationale ("checked after the indexed-file fallback so legitimately indexed documents inside the sandbox still resolve") remains correct even after this swap — the fix only moves the sandbox check before the extension guard, not before the indexed-file fallback. |
Why this matters
GAIA documents
--allowed-pathsas a security sandbox for agent file tools, but it only ever restricted writes.read_file(and three sibling read tools) inFileSearchToolsMixincalledopen()directly with no allowlist check, so an agent could read any file the process owner can access — credentials, SSH keys,.env— regardless of the configured sandbox.write_file/edit_filecorrectly blocked the same paths, and the siblingFileIOToolsMixinalready gates its reads, so reads were always meant to be confined; this one mixin just never applied the check.This was reported by an external researcher (CWE-862 / missing authorization) with a working PoC against a shipped version. After this change the PoC no longer reproduces: a read outside
allowed_pathsreturnsAccess deniedand no file content reaches the agent/LLM.The fix routes every content-reading tool in the mixin —
read_file,search_file_content,get_file_info,analyze_data_file— through a shared_read_access_errorhelper that mirrors the write-sidePathValidatorenforcement. It no-ops when no validator is attached, so hosts without a sandbox are unaffected. Affected agents: ChatAgent, DocumentQAAgent, FileIOAgent.Scope note: pure enumeration tools (
search_file,search_directory,browse_directory) are intentionally left unchanged — their documented purpose is discovering files anywhere for the user, and gating them to the allowlist would break that UX. They disclose paths/metadata, not file contents; whether discovery should also honor the sandbox is a separate product decision, not this confidentiality fix.Test plan
pytest tests/unit/test_file_tools.py— 84 pass, incl. newTestReadToolsSandbox(in-sandbox read succeeds; out-of-sandboxread_file/search_file_content/get_file_info/analyze_data_filedenied with no content leak; no-validator host still reads)pytest tests/unit/test_security_edge_cases.py test_file_write_guardrails.py test_hub_security.py— 237 pass, 6 skippedblack --check/isort --checkclean on changed files