fix: confine file tools to an allow-listed root to block path traversal#6248
fix: confine file tools to an allow-listed root to block path traversal#6248theCyberTech wants to merge 8 commits into
Conversation
LLM/prompt-injection-controlled file paths could escape the working directory. The RAG search tools and FileReadTool already routed through validate_file_path, but FileWriterTool only checked that `filename` did not escape the caller-supplied `directory` — and `directory` is itself LLM-controlled, so an agent fed untrusted content could be steered into writing anywhere on disk (e.g. ~/.ssh/authorized_keys). - safe_path: replace the single base_dir cwd jail with a deny-by-default allow-list of roots, sourced from cwd + CREWAI_TOOLS_ALLOWED_DIRS + a caller-passed allowed_dirs. Backward compatible for existing callers. - FileWriterTool: route the resolved write target through validate_file_path so writes are confined to an allow-listed root regardless of the directory argument. - Tests: allow-list extension via env/param, deny-by-default, multi-root, and a regression test for the unbounded-directory write. BREAKING: FileWriterTool no longer writes to arbitrary absolute directories by default. Set CREWAI_TOOLS_ALLOWED_DIRS to permit out-of-cwd writes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough
ChangesAllow-list path validation infrastructure and FileWriterTool integration
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Summary: This PR tightens file path validation by adding deny-by-default allow-listed roots and routes FileWriterTool writes through the shared path validation helper. No exploitable security vulnerabilities were identified in the added code.
Risk: Low risk. The changes reduce path traversal exposure for LLM-controlled file paths and do not introduce new public endpoints, authentication changes, or sensitive data handling paths.
There was a problem hiding this comment.
Pull request overview
This PR hardens crewai-tools file path handling to prevent LLM-controlled path traversal, specifically closing a gap where FileWriterTool could be directed to write outside intended directories by supplying an arbitrary directory argument.
Changes:
- Introduces a deny-by-default allow-list of permitted root directories for path validation (cwd by default, plus
CREWAI_TOOLS_ALLOWED_DIRSand optional caller-provided roots). - Updates
FileWriterToolto validate the final resolved write target viavalidate_file_path(symlink +..resolution) rather than only checking containment within the caller-supplied directory. - Adds/updates tests covering allow-list behavior and a regression test for the “unbounded directory arg” write-anywhere vulnerability.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| lib/crewai-tools/src/crewai_tools/security/safe_path.py | Expands path validation from a single base dir to a multi-root allow-list driven by cwd + env + optional parameter. |
| lib/crewai-tools/src/crewai_tools/tools/file_writer_tool/file_writer_tool.py | Routes write targets through validate_file_path to confine writes to allow-listed roots and block directory-arg escape. |
| lib/crewai-tools/tests/utilities/test_safe_path.py | Adds allow-list test coverage; minor typo in match strings noted in review comments. |
| lib/crewai-tools/tests/tools/test_file_writer_tool.py | Adjusts test environment to allow-list the temp dir and adds regression coverage for the directory-arg vulnerability. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Security Issues
- Path Traversal/Arbitrary File Write in FilesCompressorTool
Theoutput_pathprovided to FilesCompressorTool is used directly when creating archives (zip/tar) without validation. An attacker influencing tool inputs (e.g., via prompt injection into an LLM agent) can write archives to arbitrary locations on the filesystem (e.g.,/etc/cron.d/evil,~/.ssh/authorized_keys.tar.gz), bypassing the new allow-list protections intended for file operations. This violates the repository's path validation guardrail (#59) which requires validating all file paths withvalidate_file_path()before any writes.
Recommendations
- Validate
output_pathwithvalidate_file_path()before use and pass the resolved safe path into the compressor functions. Apply this both for the zip branch and the tar branch. Optionally, also validate again inside_compress_zipand_compress_tarbefore opening the archive, to ensure all call sites are safe.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/crewai-tools/src/crewai_tools/tools/files_compressor_tool/files_compressor_tool.py (1)
66-84:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPrevent self-inclusion when output archive is under the input directory.
When compressing a directory,
output_pathcan point insideinput_path. Since the archive is created before recursion, both ZIP walk and TAR add can pick up the archive being written, leading to unstable/corrupt results.💡 Suggested guard in
_runif not self._prepare_output(output_path, overwrite): return ( f"Output '{output_path}' already exists and overwrite is set to False." ) + if os.path.isdir(input_path): + input_real = os.path.realpath(input_path) + output_real = os.path.realpath(output_path) + try: + if os.path.commonpath([input_real, output_real]) == input_real: + return ( + "Error: output_path must be outside input_path when " + "compressing a directory." + ) + except ValueError: + # Different drives (common on Windows) have no common path. + pass try: format_compression = {Also applies to: 129-143, 174-177
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai-tools/src/crewai_tools/tools/files_compressor_tool/files_compressor_tool.py` around lines 66 - 84, Add a validation check in the _run method before the compression logic to ensure that output_path is not nested inside input_path. After the _prepare_output check, verify whether the output archive location falls within the input directory tree, and if so, return an error message indicating that the output path cannot be inside the input path. This guard prevents the archive from including itself during compression, which would cause corrupt or unstable results in both _compress_zip and _compress_tar operations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In
`@lib/crewai-tools/src/crewai_tools/tools/files_compressor_tool/files_compressor_tool.py`:
- Around line 66-84: Add a validation check in the _run method before the
compression logic to ensure that output_path is not nested inside input_path.
After the _prepare_output check, verify whether the output archive location
falls within the input directory tree, and if so, return an error message
indicating that the output path cannot be inside the input path. This guard
prevents the archive from including itself during compression, which would cause
corrupt or unstable results in both _compress_zip and _compress_tar operations.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a881d46e-17e7-4065-85ec-125947cbafe2
📒 Files selected for processing (2)
lib/crewai-tools/src/crewai_tools/tools/files_compressor_tool/files_compressor_tool.pylib/crewai-tools/tests/tools/files_compressor_tool_test.py
d35762d to
e0df891
Compare
…pth) Corridor scan flagged output_path reaching the archive writer. _run does validate output_path before dispatch, but _compress_zip/_compress_tar are staticmethods reachable independently of _run — called directly they would write unvalidated. Validate at the sink so every call site is confined to the allow-list, not just the _run entrypoint. Also decouple the symlink tests from the configurable-allow-list feature (PR #6248): chdir into the working dir so the allowed root is cwd, instead of setting CREWAI_TOOLS_ALLOWED_DIRS (which this branch, off main, does not yet support). Adds direct-call tests asserting the sink rejects an out-of-root output path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Security Issues
- Improper Access Control via insecure default allow-list root when CWD is '/'
The new deny-by-default allow-list builds from the current working directory (cwd) by default. If the process runs with cwd set to the filesystem root ('/'), which is common in containerized environments without a configured WORKDIR, '/' becomes an allowed root. Because '/' is a parent of all absolute paths, validate_file_path() then permits access to any path on the filesystem (subject only to OS permissions), effectively disabling the confinement and reintroducing unbounded file read/write. This undermines the intended sandbox and can be exploited by an attacker through LLM-controlled tool inputs to read or write arbitrary files outside the intended scope.
Recommendations
- Refuse to default to '/' as an allowed root. If os.getcwd() == os.sep, require an explicit allow-list via CREWAI_TOOLS_ALLOWED_DIRS or a passed base_dir, or raise a clear error.
- Alternatively, special-case '/' in _get_allowed_roots to ignore it unless explicitly provided via CREWAI_TOOLS_ALLOWED_DIRS (opt-in), not via the default.
- Document and enforce a safe default working directory for all entry points to avoid running with cwd '/'.
_get_allowed_roots defaulted its primary root to os.getcwd(). In a container started without a WORKDIR, cwd is "/", and since "/" is a parent of every absolute path the deny-by-default allow-list then permitted the entire filesystem -- silently disabling confinement and re-opening arbitrary LLM-controlled file read/write (the exact hole this PR closes). Distinguish an implicitly defaulted primary root (base_dir is None -> os.getcwd()) from operator-provided roots (base_dir, allowed_dirs, CREWAI_TOOLS_ALLOWED_DIRS). When the implicit cwd default resolves to os.sep it is dropped; an explicit "/" is still honored as a deliberate opt-in. If no usable root remains, raise a clear ValueError instead of allowing everything. Addresses the corridor-security review finding on #6248. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ED_DIRS Document the deny-by-default allow-list behavior, the new CREWAI_TOOLS_ALLOWED_DIRS env var for extending allowed roots, the fail-closed behavior when cwd is the filesystem root, and the CREWAI_TOOLS_ALLOW_UNSAFE_PATHS escape hatch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/edge/en/tools/file-document/filewritetool.mdx`:
- Line 59: The documentation on line 59 inaccurately describes how
FileWriterTool handles write failures when working directory is set to
filesystem root. The text states writes "fail with a ValueError", but
FileWriterTool actually catches this exception internally and returns an error
string/response to callers rather than throwing the exception. Rephrase the
sentence to clarify that writes are rejected with an error response (not thrown
as a ValueError), such as by saying "Writes are rejected with an error response"
or "the tool returns an error instead of completing the write" until
CREWAI_TOOLS_ALLOWED_DIRS is properly configured.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 1a70b18c-75ba-4f0e-9628-1b35a74fad77
📒 Files selected for processing (1)
docs/edge/en/tools/file-document/filewritetool.mdx
| ``` | ||
|
|
||
| <Warning> | ||
| If the process runs with its working directory set to the filesystem root (`/`) — common in containers started without a `WORKDIR` — the tool will **not** fall back to allow-listing the entire filesystem. Writes fail with a `ValueError` until you set `CREWAI_TOOLS_ALLOWED_DIRS` to an explicit directory. Set a `WORKDIR` (or the env var) in such deployments. |
There was a problem hiding this comment.
Clarify the surfaced error behavior on Line 59.
The docs currently say writes “fail with a ValueError”, but FileWriterTool catches that exception and returns an error string to callers. Consider phrasing this as a write rejection/error response instead of a thrown ValueError.
Suggested wording
- If the process runs with its working directory set to the filesystem root (`/`) — common in containers started without a `WORKDIR` — the tool will **not** fall back to allow-listing the entire filesystem. Writes fail with a `ValueError` until you set `CREWAI_TOOLS_ALLOWED_DIRS` to an explicit directory. Set a `WORKDIR` (or the env var) in such deployments.
+ If the process runs with its working directory set to the filesystem root (`/`) — common in containers started without a `WORKDIR` — the tool will **not** fall back to allow-listing the entire filesystem. Writes are rejected with an error until you set `CREWAI_TOOLS_ALLOWED_DIRS` to an explicit directory. Set a `WORKDIR` (or the env var) in such deployments.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| If the process runs with its working directory set to the filesystem root (`/`) — common in containers started without a `WORKDIR` — the tool will **not** fall back to allow-listing the entire filesystem. Writes fail with a `ValueError` until you set `CREWAI_TOOLS_ALLOWED_DIRS` to an explicit directory. Set a `WORKDIR` (or the env var) in such deployments. | |
| If the process runs with its working directory set to the filesystem root (`/`) — common in containers started without a `WORKDIR` — the tool will **not** fall back to allow-listing the entire filesystem. Writes are rejected with an error until you set `CREWAI_TOOLS_ALLOWED_DIRS` to an explicit directory. Set a `WORKDIR` (or the env var) in such deployments. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/edge/en/tools/file-document/filewritetool.mdx` at line 59, The
documentation on line 59 inaccurately describes how FileWriterTool handles write
failures when working directory is set to filesystem root. The text states
writes "fail with a ValueError", but FileWriterTool actually catches this
exception internally and returns an error string/response to callers rather than
throwing the exception. Rephrase the sentence to clarify that writes are
rejected with an error response (not thrown as a ValueError), such as by saying
"Writes are rejected with an error response" or "the tool returns an error
instead of completing the write" until CREWAI_TOOLS_ALLOWED_DIRS is properly
configured.
Problem
CrewAI's file tools accept LLM-controlled paths at runtime. When an agent processes untrusted content (a fetched page, a PDF, an upstream tool result), an injected instruction can steer it into reading or writing arbitrary files — e.g.
~/.ssh/id_rsa,~/.netrc, or~/.ssh/authorized_keys. This is not self-injection: the developer using the library as documented didn't introduce the malicious condition; the attacker did, via data.The read-side search tools (
PDF/TXT/CSV/DOCX/JSON) andFileReadTool/DirectoryReadToolalready routed throughvalidate_file_path. ButFileWriterToolonly checked thatfilenamedidn't escape the caller-supplieddirectory— anddirectoryis itself LLM-controlled, sodirectory="~/.ssh", filename="authorized_keys"passed cleanly. Write capability was effectively unbounded.Changes
security/safe_path.py— replace the implicit single-base_dircwd jail with a configurable deny-by-default allow-list of roots, sourced from cwd (default) +CREWAI_TOOLS_ALLOWED_DIRS(os.pathsep-split) + a caller-passedallowed_dirs. Symlink/..resolution and theCREWAI_TOOLS_ALLOW_UNSAFE_PATHSescape hatch are preserved. Backward-compatible for existing callers.FileWriterTool— route the resolved write target throughvalidate_file_path, confining writes to an allow-listed root regardless of thedirectoryargument.test_blocks_unbounded_directory_arg) for the core write-anywhere vuln.FileWriterToolno longer writes to arbitrary absolute directories by default; it confines to cwd unless the target dir is allow-listed viaCREWAI_TOOLS_ALLOWED_DIRS. Users who write to external absolute paths must set that env var.Testing
pytest tests/utilities/test_safe_path.py tests/tools/test_file_writer_tool.py→ 57 passed. RAG path-validation suite (51) still green. ruff + mypy clean.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
CREWAI_TOOLS_ALLOWED_DIRSto expand permitted root directories.allowed_dirssupport for configurable confinement.Bug Fixes
Documentation
Tests