On this page
In July 2026, a security researcher pointed a purpose-built scanner at every MCP server they could find on the public internet. They confirmed 640 production servers, dynamically audited 414 of them, and found 68 reportable vulnerabilities — SQL injection, prompt template injection, path traversal, and SSRF aimed squarely at cloud metadata services.
The number that should stop you: 91.8% of the servers they audited had no OAuth authentication at all.
If you run or build a web-scraping MCP server, this is your problem more than anyone else's. A scraping tool's entire job is to take a URL and fetch it. When the URL argument is chosen by a language model, and that model can be influenced by the content of pages it reads, you have built a server-side request forgery machine and handed the steering wheel to an attacker.
Table of contents
- The state of MCP server security in 2026
- Why scraping servers are the worst case
- Anatomy of a real CVE
- The bypass ladder: six ways a URL check fails
- A defense checklist that actually holds
- How CrawlForge implements this
- If you are a user, not an author
The state of MCP server security in 2026
The study is Exposed by Design: A Dynamic Security Assessment of Internet-Facing MCP Servers at Scale (Nicolás Padilla, arXiv:2608.00150, submitted 31 July 2026). It is the first dynamic behavioral assessment of MCP servers in the wild, rather than a static read of source code.
The method was passive discovery across eleven data sources — certificate transparency logs, GitHub, npm, PyPI, HuggingFace, Smithery, Censys, FOFA, Shodan and others — followed by active testing with Corvus, a framework of 34 test modules covering 10 MCP-specific vulnerability classes.
What it found across four measurement runs:
| Finding | Figure |
|---|---|
| MCP server instances detectable on the public internet | 21,000+ |
| Production servers confirmed | 640 |
| Servers dynamically audited | 414 |
| Reportable vulnerabilities found | 68 |
| Audited servers with no OAuth authentication | 91.8% |
| Tool instances exposing shell execution without access controls | 687 |
| Confirmed servers that vanished within three days | 41.6% |
That last row is the one people skip past, and it is arguably the most revealing. Four in ten servers disappeared between consecutive measurement runs — the signature of software being deployed straight to the internet without a security review, then pulled down.
Why scraping servers are the worst case
Most SSRF advice assumes a web application where the attacker has to find some obscure parameter that gets fetched server-side. A scraping MCP server inverts that completely: fetching an attacker-supplied URL is the advertised feature.
Three properties stack up badly:
The URL is model-controlled. Your tool schema says url: string. The model fills it in. Nothing in the protocol distinguishes a URL the user typed from one the model invented.
The model reads untrusted text. This is the part that turns a design property into an exploit. Your agent scrapes a page; that page contains text instructing the model to fetch http://169.254.169.254/latest/meta-data/iam/security-credentials/; the model complies. That is prompt injection converted directly into SSRF, and the request originates from inside your network with whatever credentials your instance carries.
The output goes straight back into the model's context. A classic SSRF is blind — you often cannot see the response. Here, the response body is returned to the model as tool output, and from there into the conversation, the logs, and potentially the next tool call. Exfiltration is built in.
Cloud metadata endpoints are the obvious target because they are unauthenticated by design and sit at a fixed, well-known address on every major provider. But the same primitive reaches internal admin panels, Redis on localhost, Kubernetes API servers, and anything else that trusted the network perimeter.
Anatomy of a real CVE
CVE-2026-65056 (published 21 July 2026, CWE-918, CVSS 4.0 base score 8.3 HIGH) describes this end to end in a real package. The affected software is mcp-webresearch at version 0.1.7 and below.
From the NVD record, the flaw is that the visit_page tool only validates the URL protocol — it checks that you passed http: or https: and never filters private or reserved IP ranges. The advisory then spells out the full chain: an attacker steers the LLM-controlled URL argument through prompt injection, the server's Playwright browser navigates to an internal endpoint such as a cloud instance metadata service, and the sensitive internal page content — credentials included — is returned into the model context.
That is not a theoretical attack path. That is the CVE description.
It is not an isolated case either. CVE-2026-26118 is server-side request forgery in Microsoft's own Azure MCP Server, rated CVSS 3.1 8.8 HIGH, also CWE-918, fixed in 1.0.2 and 2.0.0-beta.17. If Microsoft shipped an SSRF in an official MCP server, the odds that a weekend-project scraping server got it right are not good.
The bypass ladder: six ways a URL check fails
Here is the uncomfortable part. Most developers who do add SSRF protection add rung one or two of this ladder and stop. Each rung below is a real bypass of the rung above it.
1. Protocol-only validation. Checking for http:/https: and nothing else. This is exactly CVE-2026-65056. It stops file:// and gopher:// and nothing that matters here.
2. Hostname string blocklists. Blocking the literal strings localhost and 127.0.0.1. Trivially bypassed, because an IP address has many spellings.
3. Alternate IP literal encodings. 127.0.0.1 is also 2130706433 in decimal and 0x7f000001 in hex. The WHATWG URL parser — the one built into every modern runtime — normalizes all of these to the same address after your string check has already passed. Worse, in Node.js an IP literal never goes through DNS resolution, so guards that hook the lookup call see nothing at all.
4. IPv4-mapped IPv6. ::ffff:127.0.0.1 and ::ffff:169.254.169.254 embed an IPv4 address inside IPv6 notation. A range check that only understands dotted-quad IPv4 waves these straight through. This also opens a DNS-controlled variant: an attacker publishes an AAAA record pointing at a mapped internal address.
5. DNS rebinding (TOCTOU). You resolve the hostname, confirm it is a public address, approve it — and then the HTTP client resolves it again when it actually connects. With a short TTL, the attacker returns a public IP on the first lookup and an internal one on the second. The only durable fix is to check and then pin the connection to the validated IP, so the address you approved is the address you connect to.
6. Redirect hops. You validate the URL you were given. The server returns 302 Location: http://169.254.169.254/. Unless every hop is re-validated — and unless an allowlisted first hop does not silently unguard the rest of the chain — you have simply moved the vulnerability one step downstream.
There is a seventh case specific to browser automation: if your tool drives Playwright or Puppeteer, the browser performs its own navigation. Guarding your fetch wrapper does nothing. You need a pre-navigation check and a post-navigation re-read of the actual URL, because client-side redirects and meta-refreshes happen inside the page.
A defense checklist that actually holds
[ ] Block by resolved IP range, never by hostname string
[ ] Run the range check on IP-literal hosts too — they skip DNS entirely
[ ] Normalize IPv4-mapped IPv6 before any range comparison
[ ] Pin the connection to the validated IP (defeats DNS rebinding)
[ ] Re-validate every redirect hop, not just the first URL
[ ] Scope allowlists per-hop, so one trusted host does not unguard the chain
[ ] Guard browser navigation separately, with a post-navigation URL re-check
[ ] Block 169.254.0.0/16 (metadata), loopback, and 0.0.0.0 by default
[ ] Offer strict mode: full RFC1918 and IPv6 ULA private ranges
[ ] Put a deadline and a size cap on every response body read
[ ] Mask secrets before any telemetry or log write
[ ] Require authentication — 91.8% of audited servers do not
The ranges worth blocking by default are loopback (127.0.0.0/8, ::1), link-local including cloud metadata (169.254.0.0/16), and the unspecified address 0.0.0.0. Full private-range blocking (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7) should be available as a strict mode — plenty of legitimate deployments do need to scrape an internal staging host, and a guard nobody can configure is a guard people disable entirely.
How CrawlForge implements this
We are not writing this from the sidelines. CrawlForge's v5.0.0 release was a seven-phase remediation program, and its first phase was exactly this class of bug — including one we had shipped ourselves.
Our SSRF guard resolved hostnames and checked the resulting addresses, but did not run the same check when the host was already an IP literal. That is rung three of the ladder above, and it was live in our code. Phase 1 fixed it by running the range check on IP-literal hostnames at pre-flight and, separately, wrapping the undici dispatcher's buildConnector with a per-connect check — so a redirect hop straight to an internal address is caught at connection time even though Node never routed it through DNS.
The rest of the ladder is covered as follows:
- IPv4-mapped IPv6 normalizes to the embedded IPv4 before range checks, in both default and strict modes.
- Connections pin to the validated IP, closing the DNS-rebinding window.
- Allowlists are evaluated per hop — an approved first hop no longer unguards the redirect chain.
- Browser navigation is guarded independently:
scrape_with_actionsvalidates beforepage.gotoand re-readspage.url()afterward, closing the page if navigation landed in a blocked range. - Every fetching subsystem is wired in — page and metadata fetches, PDF downloads, webhook delivery, and research notifications each go through the guard rather than raw
fetch. - Secrets are masked before usage telemetry leaves the process.
Defaults block loopback, link-local/metadata, and 0.0.0.0. SSRF_STRICT=true adds full RFC1918 and ULA enforcement, ALLOWED_DOMAINS allowlists trusted internal hosts, and SSRF_PROTECTION_ENABLED=false exists as a kill switch for people who genuinely need one.
SSRF_STRICT=true # full private-range enforcement
ALLOWED_DOMAINS=staging.acme.dev # trusted internal targetsIf you are a user, not an author
You do not need to read anyone's source code to reduce your exposure meaningfully.
Audit what you have connected. Every MCP server in your client config runs with your machine's network access and your credentials. On a cloud VM or a corporate VPN, that reach is much wider than on a laptop.
Prefer servers that publish a security posture. A changelog that names the CVE classes it has closed tells you more than a feature list. Silence is not evidence of safety — given the study's numbers, silence is closer to evidence of the opposite.
Reduce the attack surface you expose to the model. If a server supports tool whitelisting, use it. CrawlForge takes CRAWLFORGE_TOOLS or CRAWLFORGE_TOOL_GROUPS to expose only the tools you actually need, which both cuts context bloat and shrinks what a prompt-injected model can reach for.
Treat scraped content as hostile input. It is the single most important mental shift here. Any page your agent reads can contain instructions aimed at your model. Every one of the attacks above depends on that, and no amount of network hardening fixes a workflow that pipes scraped text straight into a tool call with no review.
Building agents that read the live web? Start free with 1,000 credits — SSRF protection on by default, no configuration required. See the full docs, the scrape_with_actions reference, or how we handle anti-bot detection in stealth mode.
Try this yourself — no signup needed
Run any of CrawlForge's 30 scraping and extraction tools in the playground, then start free with 1,000 credits.
1,000 free credits • One-time • No credit card required
Tags
About the Author
Stay updated with the latest insights
Get tutorials, product updates, and web scraping tips delivered to your inbox.
No spam. Unsubscribe anytime.