Skip to content

fix: confine file tools to an allow-listed root to block path traversal#6248

Open
theCyberTech wants to merge 8 commits into
mainfrom
fix/file-tools-path-allowlist
Open

fix: confine file tools to an allow-listed root to block path traversal#6248
theCyberTech wants to merge 8 commits into
mainfrom
fix/file-tools-path-allowlist

Conversation

@theCyberTech

@theCyberTech theCyberTech commented Jun 19, 2026

Copy link
Copy Markdown
Member

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) and FileReadTool/DirectoryReadTool already routed through validate_file_path. But FileWriterTool only checked that filename didn't escape the caller-supplied directory — and directory is itself LLM-controlled, so directory="~/.ssh", filename="authorized_keys" passed cleanly. Write capability was effectively unbounded.

Changes

  • security/safe_path.py — replace the implicit single-base_dir cwd jail with a configurable deny-by-default allow-list of roots, sourced from cwd (default) + CREWAI_TOOLS_ALLOWED_DIRS (os.pathsep-split) + a caller-passed allowed_dirs. Symlink/.. resolution and the CREWAI_TOOLS_ALLOW_UNSAFE_PATHS escape hatch are preserved. Backward-compatible for existing callers.
  • FileWriterTool — route the resolved write target through validate_file_path, confining writes 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 (test_blocks_unbounded_directory_arg) for the core write-anywhere vuln.

⚠️ Breaking change

FileWriterTool no longer writes to arbitrary absolute directories by default; it confines to cwd unless the target dir is allow-listed via CREWAI_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

    • Added deny-by-default allow-list filesystem path validation.
    • Introduced CREWAI_TOOLS_ALLOWED_DIRS to expand permitted root directories.
    • Added allowed_dirs support for configurable confinement.
  • Bug Fixes

    • Hardened file writes by validating resolved targets against the allow-list (including symlinks and traversal).
    • FileWriterTool rejects targets that resolve to existing directories.
  • Documentation

    • Added a “Path confinement” section explaining configuration, failure scenarios, and unsafe-path override behavior.
  • Tests

    • Added regression coverage for blocking out-of-allow-list writes; updated safe-path assertions and allow-list behavior tests.

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>
Copilot AI review requested due to automatic review settings June 19, 2026 17:29
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

safe_path.py is reworked to a deny-by-default allow-list model: new helpers _get_allowed_roots and _is_within_root merge base_dir, a new CREWAI_TOOLS_ALLOWED_DIRS env var, and a new keyword-only allowed_dirs parameter into a resolved multi-root allow-list checked by validate_file_path and validate_directory_path. FileWriterTool._run delegates its traversal guard to validate_file_path, tests are updated to match new error messages and add allow-list coverage, and user documentation is added describing the path confinement behavior.

Changes

Allow-list path validation infrastructure and FileWriterTool integration

Layer / File(s) Summary
Allow-list helpers and validate_file/directory_path rework
lib/crewai-tools/src/crewai_tools/security/safe_path.py
Introduces _get_allowed_roots (merges base_dir, CREWAI_TOOLS_ALLOWED_DIRS env var, and allowed_dirs into a de-duplicated, realpath-resolved ordered list) and _is_within_root (safe prefix membership test). Extends validate_file_path and validate_directory_path with a keyword-only allowed_dirs parameter and rewrites their validation loops to accept any allowed root and raise a ValueError referencing the env var on rejection.
Allow-list behavior and error message tests
lib/crewai-tools/tests/utilities/test_safe_path.py
Updates dot-dot traversal, absolute-path-outside-base, and symlink-escape rejection assertions to expect new plural error message text. Adds TestAllowList covering allowed_dirs acceptance, CREWAI_TOOLS_ALLOWED_DIRS acceptance, rejection when the env var is absent, multiple os.pathsep-separated roots, cwd default behavior, and explicit root opt-in.
FileWriterTool path validation integration
lib/crewai-tools/src/crewai_tools/tools/file_writer_tool/file_writer_tool.py
Replaces manual Path.resolve() + is_relative_to() traversal guard in _run with a validate_file_path call; adds explicit rejection of targets that resolve to an existing directory; derives real_directory and display_filepath from the validated path; removes the pathlib.Path import.
FileWriterTool allow-list fixture and regression test
lib/crewai-tools/tests/tools/test_file_writer_tool.py
Updates temp_env fixture to set and restore CREWAI_TOOLS_ALLOWED_DIRS pointing at the temp directory. Adds test_blocks_unbounded_directory_arg asserting that a write to a directory outside CREWAI_TOOLS_ALLOWED_DIRS returns an error and creates no file.
FileWriterTool path confinement user documentation
docs/edge/en/tools/file-document/filewritetool.mdx
Documents the new "Path confinement" section describing allow-listed root directories, resolved path validation, default behavior (current working directory), configuration via CREWAI_TOOLS_ALLOWED_DIRS, and warnings for container scenarios and the CREWAI_TOOLS_ALLOW_UNSAFE_PATHS=true escape hatch.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the primary security fix: confining file tools to an allow-listed root to block path traversal attacks.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/file-tools-path-allowlist

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@corridor-security corridor-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_DIRS and optional caller-provided roots).
  • Updates FileWriterTool to validate the final resolved write target via validate_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.

Comment thread lib/crewai-tools/tests/utilities/test_safe_path.py
Comment thread lib/crewai-tools/tests/utilities/test_safe_path.py Outdated
Comment thread lib/crewai-tools/tests/utilities/test_safe_path.py Outdated
Comment thread lib/crewai-tools/tests/utilities/test_safe_path.py Outdated
Comment thread lib/crewai-tools/tests/utilities/test_safe_path.py Outdated
@github-actions github-actions Bot added size/L and removed size/M labels Jun 19, 2026

@corridor-security corridor-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Issues

  • Path Traversal/Arbitrary File Write in FilesCompressorTool
    The output_path provided 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 with validate_file_path() before any writes.

Recommendations

  • Validate output_path with validate_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_zip and _compress_tar before opening the archive, to ensure all call sites are safe.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Prevent self-inclusion when output archive is under the input directory.

When compressing a directory, output_path can point inside input_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 _run
         if 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

📥 Commits

Reviewing files that changed from the base of the PR and between e0df891 and d35762d.

📒 Files selected for processing (2)
  • lib/crewai-tools/src/crewai_tools/tools/files_compressor_tool/files_compressor_tool.py
  • lib/crewai-tools/tests/tools/files_compressor_tool_test.py

@theCyberTech theCyberTech force-pushed the fix/file-tools-path-allowlist branch from d35762d to e0df891 Compare June 19, 2026 17:46
@github-actions github-actions Bot added size/M and removed size/L labels Jun 19, 2026
theCyberTech added a commit that referenced this pull request Jun 19, 2026
…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>
theCyberTech and others added 5 commits June 20, 2026 11:00
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>

@corridor-security corridor-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 '/'.

Comment thread lib/crewai-tools/src/crewai_tools/security/safe_path.py
_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>
@github-actions github-actions Bot added size/L and removed size/M labels Jun 20, 2026
…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>
@mintlify

mintlify Bot commented Jun 20, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
crewai 🟢 Ready View Preview Jun 20, 2026, 3:55 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3bce3cc and ee6e542.

📒 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants