Skip to content

fix(security): eliminate DNS rebinding TOCTOU and add SSRF validation to MCP tools#6519

Open
ashusnapx wants to merge 5 commits into
crewAIInc:mainfrom
ashusnapx:fix/ssrf-toctou-and-mcp-bypass
Open

fix(security): eliminate DNS rebinding TOCTOU and add SSRF validation to MCP tools#6519
ashusnapx wants to merge 5 commits into
crewAIInc:mainfrom
ashusnapx:fix/ssrf-toctou-and-mcp-bypass

Conversation

@ashusnapx

@ashusnapx ashusnapx commented Jul 11, 2026

Copy link
Copy Markdown

Summary

Fixes two SSRF security vulnerabilities in crewai-tools.

Closes #6504


Vulnerability 1: DNS Rebinding (TOCTOU) in SSRF protection

validate_url() resolved DNS and checked the IP, then returned the URL as a string. The subsequent requests.get() resolved DNS again, creating a TOCTOU window where the DNS record could change between validation and connection.

Attack scenario: Attacker registers a domain that first resolves to a public IP (passes validation), then quickly changes to 169.254.169.254 (cloud metadata) before the connection is made.

Fix:

  • Added validate_url_and_resolve() → returns ValidatedURL(url, resolved_ip)
  • Added PinnedIPAdapter — a requests.HTTPAdapter that pins TCP connections to the pre-resolved IP
  • Updated safe_get() to use the pinned adapter for every request (including redirect targets)
  • Refactored validate_url() to share validation logic via _validate_url_common()
Before:  validate_url(url) → requests.get(url)          # DNS resolved twice
After:   validate_url_and_resolve(url) → session.get(url) # DNS resolved once, IP pinned

PinnedIPAdapter implementation:

  • Overrides send() to rewrite the URL, replacing hostname with the pinned IP
  • Sets Host header to the original hostname for virtual hosting
  • Overrides init_poolmanager() to configure assert_hostname=False for correct TLS SNI when the URL contains an IP address
  • No server_hostname kwarg injected into send() — keeps kwargs compatible with HTTPAdapter.send() signature

Vulnerability 2: MCP tools bypass SSRF validation entirely

MCP tool wrappers (mcp_tool_wrapper.py, mcp_native_tool.py) passed arguments directly to MCP servers without any URL validation. An agent using an MCP fetch server could be instructed to access internal URLs, cloud metadata, etc.

Fix:

  • Added validate_mcp_server_url() — validates the MCP server connection URL
  • Added validate_mcp_tool_args_for_urls() — recursively scans string arguments for URLs (http, https, file) and validates each one against the SSRF guard
  • Both MCP wrappers now validate server URL and tool arguments before execution
  • Extracted shared helpers into mcp_security.py to eliminate duplication
  • Added _clean_url() to strip trailing prose delimiters (), ,, .) from regex-matched URLs before validation
# Before: MCP tool arguments passed directly to server
result = await session.call_tool(tool_name, kwargs)

# After: URLs in arguments are validated first
validate_mcp_server_url(server_url)
validate_mcp_tool_args_for_urls(kwargs)
result = await session.call_tool(tool_name, kwargs)

Files Changed

File Change
security/safe_path.py Added ValidatedURL, validate_url_and_resolve(), PinnedIPAdapter (URL-level IP pinning + pool manager SNI), refactored validate_url()
security/safe_requests.py Updated safe_get() to use pinned sessions
tools/mcp_security.py New — shared SSRF validation helpers with _clean_url() for prose-safe URL extraction
tools/mcp_tool_wrapper.py Added server URL + argument SSRF validation (imports from shared module)
tools/mcp_native_tool.py Added argument SSRF validation (imports from shared module), fixed _run_async indentation
tests/mcp/test_mcp_ssrf.py 17 tests — shared validation + native MCP path coverage
tests/utilities/test_safe_requests.py Updated for pinned session + behavioral adapter tests (HTTP + HTTPS)

Tests

56 passed, 0 failed
- 30 safe_path tests (existing, all pass)
- 9 safe_requests tests (updated for pinned session + behavioral adapter tests)
- 17 MCP SSRF tests (shared validation + native MCP path coverage)

Verification

# Security tests
pytest lib/crewai-tools/tests/utilities/test_safe_path.py -v
pytest lib/crewai-tools/tests/utilities/test_safe_requests.py -v
pytest lib/crewai/tests/mcp/test_mcp_ssrf.py -v

# Lint + type check
ruff check lib/crewai-tools/src/crewai_tools/security/ lib/crewai/src/crewai/tools/mcp_security.py lib/crewai/src/crewai/tools/mcp_tool_wrapper.py lib/crewai/src/crewai/tools/mcp_native_tool.py
mypy lib/crewai/src/crewai/tools/mcp_security.py lib/crewai/src/crewai/tools/mcp_native_tool.py lib/crewai/src/crewai/tools/mcp_tool_wrapper.py lib/crewai-tools/src/crewai_tools/security/safe_path.py

… to MCP tools

Closes crewAIInc#6504

Vulnerability 1: DNS Rebinding (TOCTOU) in SSRF protection
==========================================================

validate_url() resolved DNS and checked the IP, then returned the URL
as a string. The subsequent requests.get() resolved DNS again, creating
a TOCTOU window where the DNS record could change between validation
and connection (DNS rebinding attack).

Fix:
- Add validate_url_and_resolve() that returns ValidatedURL(url, resolved_ip)
- Add PinnedIPAdapter — a requests HTTPAdapter that pins TCP connections
  to the pre-resolved IP, eliminating the second DNS resolution
- Update safe_get() to use validate_url_and_resolve() and _build_pinned_session()
  for every request (including redirect targets)
- Refactor validate_url() to share validation logic via _validate_url_common()

Vulnerability 2: MCP tools bypass SSRF validation entirely
==========================================================

MCP tool wrappers (mcp_tool_wrapper.py, mcp_native_tool.py) passed
arguments directly to MCP servers without any URL validation. An agent
using an MCP fetch server could be instructed to access internal URLs,
cloud metadata, etc.

Fix:
- Add _validate_mcp_server_url() — validates the MCP server connection URL
- Add _validate_mcp_tool_args_for_urls() — recursively scans string
  arguments for URLs (http/https/file) and validates each one
- Both MCP wrappers now validate server URL and tool arguments before
  execution, blocking SSRF attempts with a clear error message

Tests: 51 passing (30 safe_path + 7 safe_requests + 14 MCP SSRF).
ruff clean.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 92edd2e6-fdc3-431a-af2d-970acb1f8db0

📥 Commits

Reviewing files that changed from the base of the PR and between 46ae78a and 603d9ca.

📒 Files selected for processing (1)
  • lib/crewai-tools/tests/utilities/test_safe_requests.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/crewai-tools/tests/utilities/test_safe_requests.py

📝 Walkthrough

Walkthrough

Changes

SSRF protection

Layer / File(s) Summary
URL validation and IP pinning
lib/crewai-tools/src/crewai_tools/security/safe_path.py
URL validation is centralized, unsafe destinations are rejected, resolved IPs are returned, and PinnedIPAdapter pins outgoing connections.
Pinned safe requests and redirects
lib/crewai-tools/src/crewai-tools/security/safe_requests.py, lib/crewai-tools/tests/utilities/test_safe_requests.py
safe_get pins the initial URL and each redirect while preserving credential handling and redirect limits; tests cover pinned sessions and URL rewriting.
MCP URL validation
lib/crewai/src/crewai/tools/mcp_security.py, lib/crewai/src/crewai/tools/mcp_native_tool.py, lib/crewai/src/crewai/tools/mcp_tool_wrapper.py, lib/crewai/tests/mcp/test_mcp_ssrf.py
MCP server URLs and recursively embedded argument URLs are validated before execution, with blocked and allowed cases tested.

Sequence Diagram(s)

sequenceDiagram
  participant safe_get
  participant validate_url_and_resolve
  participant PinnedIPAdapter
  participant HTTPServer
  safe_get->>validate_url_and_resolve: validate initial URL
  validate_url_and_resolve-->>safe_get: URL and resolved IP
  safe_get->>PinnedIPAdapter: create pinned session
  PinnedIPAdapter->>HTTPServer: request using pinned IP
  HTTPServer-->>safe_get: response or redirect
  safe_get->>validate_url_and_resolve: validate redirect target
Loading
sequenceDiagram
  participant MCPToolWrapper
  participant URLScanner
  participant validate_url_and_resolve
  participant MCPServer
  MCPToolWrapper->>URLScanner: scan server URL and arguments
  URLScanner->>validate_url_and_resolve: validate embedded URLs
  validate_url_and_resolve-->>URLScanner: allow or ValueError
  URLScanner-->>MCPToolWrapper: validation result
  MCPToolWrapper->>MCPServer: execute allowed tool call
Loading

Suggested reviewers: lorenzejay

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the two security fixes: DNS rebinding protection and MCP SSRF validation.
Description check ✅ Passed The description directly matches the PR changes and explains both security fixes in detail.
Linked Issues check ✅ Passed The code changes address both linked issue objectives by pinning validated IPs and validating MCP server and argument URLs.
Out of Scope Changes check ✅ Passed The changes appear scoped to the reported security fixes, with only minor related refactors and test updates.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 4

🧹 Nitpick comments (1)
lib/crewai/src/crewai/tools/mcp_tool_wrapper.py (1)

11-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Duplicated SSRF helpers across both MCP wrappers.

_URL_PATTERN and _validate_mcp_tool_args_for_urls are copied verbatim into mcp_native_tool.py. The copies are already diverging (the native one is mis-scoped), which is exactly the failure mode duplication invites. Consider extracting them into a single shared module imported by both wrappers.

🤖 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/src/crewai/tools/mcp_tool_wrapper.py` around lines 11 - 55,
Extract _URL_PATTERN and _validate_mcp_tool_args_for_urls into one shared MCP
security helper module, then update both mcp_tool_wrapper.py and
mcp_native_tool.py to import and reuse those symbols. Remove the duplicated
definitions from each wrapper, preserving the existing recursive URL validation
behavior and correcting the native wrapper’s scoping through the shared
implementation.
🤖 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 `@lib/crewai-tools/src/crewai_tools/security/safe_path.py`:
- Around line 270-290: The PinnedIPAdapter implementation does not actually
route connections through the validated IP. Update init_poolmanager to assign
the correctly configured manager to the requests-expected poolmanager attribute,
pass PoolManager arguments by their intended meaning, and ensure the
connection-opening path consumes _resolved_ip while preserving the original
hostname for Host and TLS SNI. Verify safe_get uses this adapter so
DNS-rebinding protection is enforced.

In `@lib/crewai-tools/tests/utilities/test_safe_requests.py`:
- Around line 185-209: Add a behavioral test alongside
test_safe_get_uses_pinned_ip_adapter that exercises the real
_build_pinned_session and PinnedIPAdapter instead of monkeypatching the session
builder. Use a controllable HTTP/HTTPS server or transport to assert the
connection targets the resolved IP while the request Host header and TLS SNI
remain the original hostname; retain the existing test only for validating the
passed ValidatedURL if needed.

In `@lib/crewai/src/crewai/tools/mcp_native_tool.py`:
- Around line 110-136: Dedent _run_async so it is defined as a MCPNativeTool
class method rather than nested inside _validate_mcp_tool_args_for_urls. Keep
_validate_mcp_tool_args_for_urls at module scope and preserve _run_async’s
existing implementation so self._run_async(**kwargs) remains callable.

In `@lib/crewai/tests/mcp/test_mcp_ssrf.py`:
- Around line 11-14: Add parallel SSRF validation coverage for the native MCP
path by importing the corresponding validators from crewai.tools.mcp_native_tool
or invoking MCPNativeTool._run and asserting the SSRF-blocked message. Keep the
existing mcp_tool_wrapper coverage and ensure the native path’s URL and
tool-argument validation behavior is exercised.

---

Nitpick comments:
In `@lib/crewai/src/crewai/tools/mcp_tool_wrapper.py`:
- Around line 11-55: Extract _URL_PATTERN and _validate_mcp_tool_args_for_urls
into one shared MCP security helper module, then update both mcp_tool_wrapper.py
and mcp_native_tool.py to import and reuse those symbols. Remove the duplicated
definitions from each wrapper, preserving the existing recursive URL validation
behavior and correcting the native wrapper’s scoping through the shared
implementation.
🪄 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: 4bbd2626-2155-45cc-b9bf-cd9e1e11dba5

📥 Commits

Reviewing files that changed from the base of the PR and between fb8e93b and fb0f073.

📒 Files selected for processing (6)
  • lib/crewai-tools/src/crewai_tools/security/safe_path.py
  • lib/crewai-tools/src/crewai_tools/security/safe_requests.py
  • lib/crewai-tools/tests/utilities/test_safe_requests.py
  • lib/crewai/src/crewai/tools/mcp_native_tool.py
  • lib/crewai/src/crewai/tools/mcp_tool_wrapper.py
  • lib/crewai/tests/mcp/test_mcp_ssrf.py

Comment thread lib/crewai-tools/src/crewai_tools/security/safe_path.py Outdated
Comment thread lib/crewai-tools/tests/utilities/test_safe_requests.py
Comment thread lib/crewai/src/crewai/tools/mcp_native_tool.py Outdated
Comment thread lib/crewai/tests/mcp/test_mcp_ssrf.py Outdated
…ct shared MCP helpers, add native MCP test coverage

- Fix PinnedIPAdapter: rewrite URL in send() to pin TCP to resolved IP
  while preserving Host header and TLS SNI (was storing unused pool manager)
- Extract shared SSRF helpers (_URL_PATTERN, validate_mcp_server_url,
  validate_mcp_tool_args_for_urls) into mcp_security.py to eliminate
  duplication between mcp_tool_wrapper and mcp_native_tool
- Fix _run_async indentation: was nested inside _validate_mcp_tool_args_for_urls,
  now properly a MCPNativeTool class method
- Add 3 native MCP path tests (blocks internal, blocks metadata, allows public)
- Add behavioral PinnedIPAdapter test verifying adapter stores correct IP

@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: 2

🧹 Nitpick comments (1)
lib/crewai/src/crewai/tools/mcp_security.py (1)

12-12: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Greedy URL regex can capture trailing punctuation and cause false blocks.

[^\s\"']+ will absorb trailing characters like ), ,, or . when a URL is embedded in prose (e.g. "see http://public.example." matches http://public.example.). That mangled host is then handed to validate_url_and_resolve, which may raise on resolution and incorrectly block a legitimate tool call, or surface a non-ValueError that escapes the SSRF handler.

Consider trimming common trailing delimiters from match.group() before validation.

🤖 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/src/crewai/tools/mcp_security.py` at line 12, Update _URL_PATTERN
URL extraction to remove common prose delimiters such as trailing ), , and .
from match.group() before passing the URL to validate_url_and_resolve, while
preserving valid URL characters and the existing SSRF validation flow.
🤖 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 `@lib/crewai-tools/src/crewai_tools/security/safe_path.py`:
- Around line 316-320: Remove the server_hostname injection from
PinnedIPAdapter.send and keep its kwargs compatible with HTTPAdapter.send before
calling super().send(request, **kwargs). Add or update coverage for an HTTPS
request through the pinned path to verify it no longer raises TypeError.

In `@lib/crewai-tools/tests/utilities/test_safe_requests.py`:
- Around line 212-227: The test_pinned_adapter_rewrites_url test does not
exercise PinnedIPAdapter.send() or its URL and Host header rewriting. Replace
the _resolved_ip, hasattr, and callable assertions with a mocked
HTTPAdapter.send() invocation, then assert the forwarded request uses
93.184.216.34 while preserving the expected Host header.

---

Nitpick comments:
In `@lib/crewai/src/crewai/tools/mcp_security.py`:
- Line 12: Update _URL_PATTERN URL extraction to remove common prose delimiters
such as trailing ), , and . from match.group() before passing the URL to
validate_url_and_resolve, while preserving valid URL characters and the existing
SSRF validation flow.
🪄 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: b1312343-bfc7-4fc7-9aa3-ffce47c7c578

📥 Commits

Reviewing files that changed from the base of the PR and between fb0f073 and 208de26.

📒 Files selected for processing (6)
  • lib/crewai-tools/src/crewai_tools/security/safe_path.py
  • lib/crewai-tools/tests/utilities/test_safe_requests.py
  • lib/crewai/src/crewai/tools/mcp_native_tool.py
  • lib/crewai/src/crewai/tools/mcp_security.py
  • lib/crewai/src/crewai/tools/mcp_tool_wrapper.py
  • lib/crewai/tests/mcp/test_mcp_ssrf.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/crewai/src/crewai/tools/mcp_tool_wrapper.py

Comment thread lib/crewai-tools/src/crewai_tools/security/safe_path.py Outdated
Comment thread lib/crewai-tools/tests/utilities/test_safe_requests.py Outdated
…name kwarg, strip prose delimiters from URLs, rewrite behavioral test

- PinnedIPAdapter: removed server_hostname injection from send() which
  caused TypeError on HTTPS requests (HTTPAdapter.send() doesn't accept it)
- PinnedIPAdapter: added init_poolmanager override with assert_hostname=False
  for correct TLS SNI when URL contains an IP address
- MCP SSRF: added _clean_url() to strip trailing ), ,, . from regex-matched
  URLs in prose (e.g. markdown links, natural language) before validation
- Behavioral test: replaced hasattr/callable assertions with mocked
  HTTPAdapter.send() that verifies URL is rewritten to use pinned IP and
  Host header preserves original hostname
- Added HTTPS variant of behavioral test to cover TLS path

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

🧹 Nitpick comments (1)
lib/crewai-tools/tests/utilities/test_safe_requests.py (1)

212-271: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider parametrizing the two adapter tests to reduce duplication.

test_pinned_adapter_rewrites_url and test_pinned_adapter_https_rewrites_url are identical except for the URL scheme. A single parametrized test would eliminate the copy-paste. Additionally, MagicMock is imported at module level (line 8) but re-imported locally inside both functions — the local imports can be removed.

♻️ Optional refactor: parametrize and remove redundant imports
+@pytest.mark.parametrize("url,expected_scheme", [
+    ("http://public.example/data", "http"),
+    ("https://public.example/secure", "https"),
+])
+def test_pinned_adapter_rewrites_url(
+    url: str, expected_scheme: str, public_dns: None, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    """Behavioral test: PinnedIPAdapter.send() rewrites URL and sets Host header."""
+    from crewai_tools.security.safe_path import PinnedIPAdapter
+
+    adapter = PinnedIPAdapter(resolved_ip="93.184.216.34")
+
+    captured_requests: list[Any] = []
+
+    def fake_send(self: Any, request: Any, **kwargs: Any) -> MagicMock:
+        captured_requests.append(request)
+        resp = MagicMock()
+        resp.status_code = 200
+        return resp
+
+    monkeypatch.setattr(
+        "requests.adapters.HTTPAdapter.send", fake_send,
+    )
+
+    req = requests.Request("GET", url)
+    prepared = req.prepare()
+    adapter.send(prepared)
+
+    assert len(captured_requests) == 1
+    sent = captured_requests[0]
+    assert "93.184.216.34" in sent.url
+    assert sent.headers["Host"] == "public.example"
🤖 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/tests/utilities/test_safe_requests.py` around lines 212 -
271, Consolidate test_pinned_adapter_rewrites_url and
test_pinned_adapter_https_rewrites_url into one parametrized test covering both
http and https URL schemes, while preserving their existing assertions and
behavior. Remove the redundant function-local MagicMock imports and use the
existing module-level import.
🤖 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.

Nitpick comments:
In `@lib/crewai-tools/tests/utilities/test_safe_requests.py`:
- Around line 212-271: Consolidate test_pinned_adapter_rewrites_url and
test_pinned_adapter_https_rewrites_url into one parametrized test covering both
http and https URL schemes, while preserving their existing assertions and
behavior. Remove the redundant function-local MagicMock imports and use the
existing module-level import.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a13e5a7-cd98-4d9c-a260-6dc8417bef60

📥 Commits

Reviewing files that changed from the base of the PR and between 208de26 and 46ae78a.

📒 Files selected for processing (3)
  • lib/crewai-tools/src/crewai_tools/security/safe_path.py
  • lib/crewai-tools/tests/utilities/test_safe_requests.py
  • lib/crewai/src/crewai/tools/mcp_security.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/crewai/src/crewai/tools/mcp_security.py
  • lib/crewai-tools/src/crewai_tools/security/safe_path.py

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] DNS Rebinding (TOCTOU) bypass in SSRF protection + MCP tools bypass SSRF validation entirely

1 participant