Agent framework SSRF validation is the security question almost nobody has answered at the class level: when a model-directed fetch tool follows an HTTP redirect, does the framework re-check where the request is actually going? This census scores fourteen agent frameworks’ fetch tools — fifteen rows, because Anthropic’s hosted and local tools earn separate entries — against five concrete SSRF controls, every row pinned to a released version with a primary source — or, where no version applies, to the date that source was read.
The stakes are not hypothetical. A fetch tool that validates only the first URL and then follows redirects blindly can be steered from a harmless public page to 169.254.169.254, an internal admin panel, or localhost — the textbook server-side request forgery pattern that MITRE catalogues as CWE-918, ranked #14 in the 2024 CWE Top 25. Agent tool loops make it worse: the URL often comes from a model, and the model’s input often comes from an attacker.
What follows is the method, the full fifteen-row table, a close look at the few implementations that validate every hop, the zero-validation tier, the tools whose protection exists but ships switched off, and the two competing threat-model philosophies the table reveals. Every cell is Yes, No, or — where no primary source answers the question — an honest “not established.”
- 01Per-hop redirect validation is the exception.Of fifteen census rows, only CrewAI (1.15.17, August 20) and PydanticAI (1.56.0, February CVE fix) validate every redirect hop on their default fetch paths, plus LangChain.js's tool-specific RecursiveUrl fix.
- 02The MCP reference fetch server has no SSRF logic at all.The spec's own worked example calls httpx with follow_redirects=True at two call sites and contains no private-IP, metadata, peer-IP, or allowlist check — confirmed by direct source reads in March and again on August 23.
- 03Every validator got there through a disclosure, not by design.PydanticAI's _ssrf.py module shipped as the fix for CVE-2026-25580, and LangChain.js's per-hop re-validation as the fix for CVE-2026-27795. CrewAI's per-hop peer-IP pinning arrived in the 1.15.17 bug-fix release of August 20.
- 04“Does it validate” and “is it on by default” are different questions.n8n has real SSRF protection — shipped disabled since 2.12.0. Mastra's redirect-hop blocking activates only when a developer sets allowedHosts. Dify delegates enforcement to a Squid proxy outside its application code.
- 05Honest gaps beat guessed verdicts.Semantic Kernel, the OpenAI Agents SDK's hosted tool, and the Claude Agent SDK's local WebFetch keep “not established” cells: no primary source answers the question, and inferring one framework's behaviour from a sibling is how bad security tables get written.
01 — Threat ModelWhy the redirect hop is the whole game. #
Server-side request forgery is old. What agent frameworks changed is who chooses the URL. In a classic web app, a developer decides which endpoints the server contacts; in an agent loop, the model does — and the model’s decision is shaped by whatever content it just read. A fetch tool is, in CWE-918’s terms, a server that retrieves a URL received from an upstream component. The upstream component is now an LLM.
Validating the first URL is not enough. The OWASP SSRF Prevention Cheat Sheet recommends disabling automatic redirect-following entirely, and — when redirects must be supported — advises to “retrieve all IP addresses behind a domain name (both A and AAAA records)” and validate the resolved address at every hop, rejecting the whole request if any hop lands on internal or reserved space. A validator that checks only hop zero can be bypassed by any attacker who controls a public URL that 302s to a private one.
The prize behind the redirect is usually a cloud metadata endpoint. On AWS, IMDSv2’s default hop limit of 1 means a stolen session token generally cannot be relayed off-instance through a standard SSRF proxy chain — unless an operator raised HttpPutResponseHopLimit
to 2, as is typical for containerized workloads. On GCP, the current v1 metadata API requires the Metadata-Flavor: Google
header — a control added specifically to break header-less SSRF requests — while legacy v0.1/v1beta1 endpoints enforce nothing and remain a risk where not explicitly disabled. An agent fetch tool with no metadata-endpoint block hands an attacker a path to exactly these surfaces.
One scoping note: SSRF is a network-layer weakness, distinct from prompt injection, which attacks the model’s instructions rather than the server’s egress. For that other class, see the Context7 prompt-injection CVE analysis. The two often chain — injected instructions pick the malicious URL, missing egress validation lets it land — but they are fixed in different places, and this census measures only the second.
#14in the 2024 CWE Top 25. Every column in this census is a concrete test of “sufficiently ensure.”
02 — MethodFive controls, version pins, and the discipline of not guessing. #
Each row answers the same five questions about a framework’s model-facing fetch or HTTP tool as it stood on or before August 23, 2026 — pinned to a named released version where one applies, and otherwise to the date the source was read:
(a) Per-hop re-validation— does it re-validate the destination, ideally the resolved peer IP, on every redirect hop rather than only the initial URL?(b) Metadata endpoints— does it block link-local and cloud-metadata addresses such as 169.254.169.254?(c) Private ranges— does it block private and loopback ranges by default?(d) Domain allowlist— does it support restricting fetches to an operator-defined set of hosts?(e) Redirect-depth cap— does it bound how many hops it will follow?
Three rules kept the table honest. First, every Yes or No traces to a primary source — a GitHub security advisory, a release note, a vendor documentation page, or a direct read of the shipped source file — cited on the row. Second, no framework’s behaviour was inferred from a sibling: the fix that hardened LangChain.js’s RecursiveUrl
says nothing about the Python langchain-community
request tools, so the Python row carries its own cells. Third, a latest-package version is a freshness signal, not re-verification — CVE-driven rows cite the fix version and date, not today’s release number.
This is the third entry in our census series, after the coding-agent permission-defaults census and the data-terms census of the same tooling landscape. Same format contract: one claim per cell, one source per claim, and no verdict column — the table reports observable behaviour, not a “vulnerable / safe” stamp.
Established from primary source
The cell quotes or directly paraphrases a dated primary source. Source reads were done on the shipped file in the repository's main branch; advisory-backed cells cite the advisory's own fix description.
An honest gap, not a defect finding
Hosted tools whose network layer is not open source, and frameworks whose docs are silent, get “not established” cells. That is a legitimate row state — far better than inferring behaviour the vendor never published.
The tool does not exist
Mastra ships no built-in “visit this URL” tool for the model, and the OpenAI Agents SDK's WebSearchTool executes on hosted infrastructure. Forcing those rows into the same columns as a local fetch tool would manufacture a comparison.
03 — The DatasetThe census: 15 rows, five controls, one claim per cell. #
The table below is the deliverable. Rows are grouped by what the sources established: per-hop validators, confirmed-absent validation, delegated or opt-in enforcement, and hosted or not-established rows. Framework names link to the primary source each row was scored against. Data ceiling: August 23, 2026. The table scrolls horizontally.
| # | Framework · fetch surface · source | Version pinned | (a) Re-validates every hop | (b) Blocks metadata / link-local | (c) Blocks private / loopback | (d) Domain allowlist | (e) Redirect-depth cap |
|---|---|---|---|---|---|---|---|
| Rows 1–3 · Per-hop re-validation shipped, established from advisories and release notes | |||||||
| 1 | CrewAI — safe_get fetch path | ||||||
Yes. Release note, verbatim: “Pin SSRF checks to each redirect hop and peer IP.” Location-header re-validation per hop since the June fix; 1.15.17 pins the resolved peer IP.GHSA-2jrp-274c-jhv3 · CVE-2026-25580
Yes. Advisory, verbatim: “Each redirect validated before following (max 10).”Yes — unconditional. 169.254.169.254, fd00:ec2::254, 100.100.100.200 stay blocked even under force_download='allow-local'GHSA-mphv-75cg-56wg · CVE-2026-27795
PR #9990 · @langchain/core/utils/ssrf Yes. Auto-follow disabled; the Location header is re-validated at every hopmodelcontextprotocol/servers · src/fetch
No. follow_redirects=True at two call sites (robots.txt and main fetch), no re-validationrun-llama/llama_index · simple_web/base.py
No. URLs pass straight to requests.get(); redirects followed by library default with no re-validationhuggingface/smolagents · default_tools.py
No. Bare requests.get(url, timeout=20) that inherits the library default of following up to 30 redirectsmicrosoft/autogen · _http_tool.py
langgenius/dify · ssrf_proxy.py
[n8n-io/n8n · Issue #28035 (CWE-918)](https://github.com/n8n-io/n8n/issues/28035)
[docs.n8n.io · SSRF protection](https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables/ssrf-protection)
**disabled by default**[mastra.ai · MCPClient reference](https://mastra.ai/reference/tools/mcp-client)
[platform.claude.com · web fetch tool docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool)
[third-party system-prompt mirror (not an Anthropic primary source)](https://github.com/Piebald-AI/claude-code-system-prompts/blob/main/system-prompts/tool-description-webfetch.md)
[openai.github.io · Agents SDK tools docs](https://openai.github.io/openai-agents-python/tools/)
learn.microsoft.com · OpenApiFunctionExecutionParameters
[GHSA-h5gc-rm8j-5gpr · CVE-2025-2828](https://github.com/advisories/GHSA-h5gc-rm8j-5gpr)
[pypi.org · langchain-community 0.4.2](https://pypi.org/project/langchain-community/)
Reading rule for every cell: Yes and No are established claims tied to the pinned version and the linked source; “not established” means no primary source answered the question — deliberately not a guess, and not a verdict of vulnerability. The two LangChain rows are the cautionary pair: an ecosystem can carry a genuinely strong fix on one side (JS) while its sibling package’s equivalent tools have no published per-hop fix at all — assuming they share one would be exactly the inference this census refuses to make.
04 — The ValidatorsWho validates every hop — and how they got there. #
Three codebases in the census re-validate redirects hop by hop, and all three arrived at it the same way: a disclosure. None shipped per-hop validation on day one.
CrewAI is the freshest: version 1.15.17, released August 20, 2026, carries the release-note line “Pin SSRF checks to each redirect hop and peer IP.” The incident behind that line, and why peer-IP pinning is the part most validators still miss, is in our CrewAI redirect-hop deep dive— this census takes those findings as one row’s inputs and moves on.
PydanticAI holds the strongest single row. CVE-2026-25580 (published February 6, 2026) documented an SSRF path in the framework’s download_item()
helper, where an attacker-supplied URL in untrusted message history could reach internal network resources. The fix, shipped in 1.56.0, introduced a dedicated _ssrf.py
module: every redirect validated before following, capped at 10 hops; private ranges blocked by default; and cloud-metadata endpoints blocked unconditionally — even when a caller explicitly opts into local access. That is the strongest per-hop-plus-metadata combination in the table.
LangChain.js shows what the vulnerable pattern looks like written down. CVE-2026-27795 hit RecursiveUrl
in @langchain/community
≤ 1.1.17, and the advisory’s description of the bug is the cleanest statement of the entire problem class this census measures:
"SSRF validation (validateSafeUrl) is only performed on the initial URL. Redirects are followed automatically by fetch (redirect: "follow" default), so the request can change destinations without additional validation."— GitHub Security Advisory GHSA-mphv-75cg-56wg (LangChain.js CVE-2026-27795)
The patch — @langchain/community
1.1.18, requiring @langchain/core
≥ 1.1.21 — disables automatic redirect-following and re-validates the Location header at every hop against a new @langchain/core/utils/ssrf
module (PR #9990, merged February 11) that blocks RFC 1918 ranges, loopback, link-local addresses, and AWS/GCP/Azure metadata endpoints, with the PR noting that cloud metadata stays blocked even with the allowPrivate
flag set. The scope caveat matters as much as the fix: this is JS-side and tool-specific. No equivalent published per-hop fix was found for the Python langchain-community
request tools as of August 23 — which is why the census carries two LangChain rows instead of one.
escape hatch that doesn’t escape everything: even when a developer passes
force_download='allow-local'
to deliberately reach private addresses, 169.254.169.254 and its IPv6 and Alibaba equivalents stay unreachable. Overrides that preserve a hard floor are the standard the rest of this table should be measured against — an operator convenience should never be able to hand over the credential endpoint.## 05 — The Zero TierThe zero-validation tier includes the spec’s own reference implementation.
Four rows established, by direct source read, that no SSRF validation exists at all. The most consequential is the official MCP reference fetch server — modelcontextprotocol/servers, src/fetch
— the worked example developers copy when they build their first MCP tool. Its server.py
calls httpx.AsyncClient
with follow_redirects=True
at two call sites and contains no private-IP check, no per-hop re-validation, no peer-IP pinning, and no allowlist. That was verified twice: a direct read dating the file’s most recent edit to March 15, 2026, and an independent re-fetch of the live main branch on August 23, 2026, confirming the same call sites and the same absence of any SSRF-related term in the file.
The rest of the tier is ordinary in the worst way. LlamaIndex’s SimpleWebPageReader
passes URLs directly to requests.get()
. smolagents’ VisitWebpageTool
is a bare requests.get(url, timeout=20)
whose only safety features are the 20-second timeout and 40,000-character truncation. AutoGen’s HttpTool
accepts any host and port with no restriction — though whether it follows redirects at all is genuinely unconfirmed, since httpx defaults to follow_redirects=False
and the constructor sets nothing; the absence of IP and metadata validation is confirmed regardless of that answer.
SSRF checks in server.py
Two httpx call sites with follow_redirects=True — robots.txt and the main fetch — and no private-IP, metadata, peer-IP, or allowlist logic anywhere in the file. Confirmed by direct reads in March and again on August 23, 2026.
Timeout — the only guard
VisitWebpageTool is a bare requests.get(url, timeout=20) with 40,000-character content truncation. No IP validation, no hop re-validation, no allowlist — and requests follows up to 30 redirects by default.
Host and port accepted
HttpTool's constructor accepts any host and port with no restriction, and the source contains no IP or metadata check. Whether it follows redirects at all is unconfirmed — httpx defaults to follow_redirects=False.
The reference-server finding deserves its own sentence: the protocol’s own example — the code most likely to be copied into a thousand internal fetch servers — fails all five census columns. Nothing about the label “reference implementation” implies “hardened,” and anyone running an MCP fetch server inherited from that template should audit it against this table’s columns. For the wider server surface beyond fetch — transport, auth, tool descriptions — see our 75-point MCP server security audit.
06 — DefaultsProtection that exists but ships switched off. #
“Does it validate” and “is validation on by default” are two different questions that almost every piece of SSRF coverage collapses into one. Three census rows sit in the gap between them: real, sometimes sophisticated protection that a stock install never runs. The table below separates the default state, the action required, and — the most interesting column — what stays protected even when an operator opts out or fails to opt in.
| Tool · control | Default state as shipped | What you must do to get protection | What stays protected regardless |
|---|---|---|---|
| n8n — SSRF protection for the HTTP Request nodesince 2.12.0 · pinned at 2.35.7 (2026-08-21) | Off. N8N_SSRF_PROTECTION_ENABLED ships false — a stock install can reach internal services, metadata endpoints, and localhost. Tracked as a CWE-918 issue (CVSS 3.1: 7.5, High), reported 2026-04-04. | Set the environment variable; review the blocked/allowed CIDR list (defaults cover private ranges, loopback, link-local/reserved). One report (Issue #28218) states protection only fully activates once credential domains are configured on the relevant node — verify, don’t assume. | Nothing — with the flag off, no SSRF validation runs at all. |
| Mastra — MCPClient allowedHosts@mastra/core 1.63.2 (npm latest, 2026-08-23) | Unset. No model-facing fetch tool ships; the redirect-hop control exists only on the MCP connection surface and only when configured. | Set allowedHosts on an HTTP-transport MCP server definition. Docs: on the default fetch path, requests to disallowed hosts — “including every redirect hop” — are blocked before they are sent. | Nothing on a custom fetch function — it bypasses the control entirely and must enforce its own redirect policy. |
| PydanticAI — force_download='allow-local'_ssrf.py · fixed in 1.56.0 (CVE-2026-25580) | On. The inverse case: per-hop validation, private-range blocking, and the 10-hop cap all run by default after 1.56.0. | Nothing — protection is the default. The explicit action here goes the other way: opting out of the private-range block for legitimate local fetches. | Cloud-metadata endpoints — 169.254.169.254, fd00:ec2::254, 100.100.100.200 — stay blocked unconditionally, even under the explicit local-access override. |
The n8n row is the one to internalize. It would be wrong to say “n8n doesn’t validate” — it ships a genuinely thorough validator that checks redirect targets and DNS resolution against a CIDR list, resisting the DNS-rebinding bypass that defeats naive per-hop checks. It is equally wrong to say “n8n is protected” — because the stock answer is no. For self-hosted workflow platforms, the census cell that matters is not the feature list; it is the shipped default.
07 — Two DesignsValidate the destination, or control the provenance. #
Step back from the cells and the table splits into two coherent security philosophies. The first — validate the destination — lets the model request any URL and checks where the request actually lands: CrewAI’s per-hop peer-IP pinning, PydanticAI’s _ssrf.py
, LangChain.js’s per-hop Location re-validation. The second — control the provenance — is Anthropic’s hosted web_fetch
design: Claude may only fetch URLs that already appeared in the conversation context, and the docs state flatly that “Claude is not allowed to dynamically construct URLs.” That sidesteps most redirect-hop games by never letting the model invent a target in the first place, and layers domain filtering (allowed_domains
/ blocked_domains
) plus a documented url_not_allowed
error for private addresses on top.
Neither philosophy is strictly better; they answer different threat models. Destination validation assumes hostile URLs will arrive and hardens the egress path — auditable in open source, but only as strong as its weakest hop check. Provenance control shrinks the attack surface before egress — but its enforcement lives on hosted infrastructure that outsiders cannot audit, which is exactly why several of that row’s cells stay “not established.” The interpretive trend in this table is blunter, though: every open-source per-hop validator got there through a CVE or a disclosed incident, none by original design. SSRF hardening in agent frameworks is being retrofitted one disclosure at a time, which predicts where the next disclosures land: the zero-tier rows and the default-off installs.
Looking forward, expect per-hop validation to become table stakes the same way allow_dangerous_requests
-style gates did after the 2025 LangChain CVE — and expect the frameworks without a dedicated SSRF module today to grow one within a release cycle or two of their first public incident. Until then, the practical posture is defense outside the framework: an egress proxy in the Dify style, or a validated fetch wrapper in the OWASP style, mounted in front of any tool from rows 4 through 7. If you run agents that fetch model-supplied URLs in production, this is the kind of review our AI transformation engagements start with — the census above is literally the checklist.
CrewAI · PydanticAI patterns
If your stack already sits on row 1–3, pin the fix version or later and treat those implementations as reference designs — especially PydanticAI's unconditional metadata floor and CrewAI's peer-IP pinning.
Wrap before you mount
MCP reference fetch, LlamaIndex readers, smolagents, AutoGen HttpTool: put an egress proxy or a validated fetch wrapper in front, per OWASP — resolve A and AAAA records and re-check every hop, or disable redirects outright.
Flip the switch, then verify
n8n: set N8N_SSRF_PROTECTION_ENABLED and confirm it actually engages on the nodes you use. Dify: keep the Squid layer in the deployment and manage the allowlist in squid.conf.template — the app code enforces nothing on its own.
Domain filters, not audits
Anthropic's web_fetch and OpenAI's hosted search cannot be independently audited. Use the documented controls you do get — allowed_domains, provenance restrictions — and treat unpublished internals as exactly that.
08 — ConclusionA table worth maintaining. #
Five controls, fifteen rows, no guesses.
The headline finding is uncomfortable but simple: as of August 23, 2026, per-hop redirect validation is the exception rather than the norm, the protocol’s own reference fetch server carries no SSRF logic at all, and the implementations that do validate every redirect hop — CrewAI’s and PydanticAI’s default paths, plus LangChain.js’s -specific fix — all earned it through disclosures rather than design.
The census format is the other half of the point. One claim per cell, one primary source per claim, versions or source-read dates pinned, and not established written where the evidence runs out — because a security table that guesses is worse than no table. Framework maintainers who find their row incomplete have a straightforward remedy: publish the behaviour, and the cell changes.
If you build on any of these frameworks, the action list is short. Find your row. If it validates, pin the fix version. If it doesn’t, wrap it. If protection exists but ships off, turn it on and verify it engages. And whichever row you sit on, keep metadata endpoints unreachable by a control the framework cannot override — that is the one cell where PydanticAI has already shown everyone the standard.