Stay up to date on Tachyon.
Trivial To Introduce, Impossible to Fix: Why SSRFs are the Trickiest Security Issue in Modern Web Apps
One line of code introduces an SSRF. Fixing it correctly requires aligning URL parsing, DNS resolution, redirect handling, HTTP client behavior, and network policy—all at once, without missing a single edge case.
SSRFs (Server-Side Request Forgeries) are one of the most common software vulnerabilities. Tachyon already has found several.
The scenario is simple. A website accepts a user-provided URL—say, as a way to import a profile picture. Then the server fetches the URL and returns the output to the user. If you implement that the obvious way, via something like requests.get(user_input), then congrats: you've just introduced an SSRF.
When the server fetches the URL, it still retains its own identity. It then implicitly applies the privileges of its identity to a user-directed target. What happens if the user enters, say, the IP of the AWS metadata service into your profile picture URL field? Now your VPS will make a request to the service, successfully authenticate (as it is indeed an EC2 instance), and blindly return the info to the user.
That is SSRF: attacker-controlled input makes your server send requests on the attacker's behalf. Along with the above example, SSRFs can also let an attacker probe network topology from inside your trust boundary, or chain into privilege escalation and lateral movement.
And unlike most other injection classes, the fix isn't obvious. The attack surface spans multiple layers all at once, from DNS resolution to network egress. You can't just blocklist a single URL because that URL can be obfuscated and represented in various other ways. Even a comprehensive blocklist isn't enough because the attacker can control their own DNS.
Contrast this to, say, cross-site scripting (XSS), where the fix is indeed obvious: you treat all input explicitly as a string.
Additionally, consider how often you need SSRF-able functionality in web applications. Most applications don't need to execute user code, and those that do can isolate it to one area. But URL-fetching is required everywhere. Thumbnail previews, webhooks, OAuth, AI chats, link unfurling, PDF imports—these are all table-stakes for the modern web, and they're also major potential vulnerabilities.
The reality is, SSRFs are trivial to introduce and yet incredibly difficult to correctly fix because the real security boundary is not a parameter check or a single sanitization point. It's an amorphous combination of URL parsing, DNS resolution, redirect handling, HTTP client behavior, and egress policy, that all need to be aligned to ensure your app is secure. And because of this, despite being one of the oldest vulnerabilities in software, SSRF is still common, dangerous, and very hard to kill.
Why SSRF Is So Hard to Fix
Other injection issues are also widespread, and have severe security impact. However, most of the time, their primary controls are localized.
- XSS is often centered on output encoding and rendering boundaries.
- Path traversal is often centered on path normalization and root-directory enforcement.
- SQL-injection touches the database-boundary and is centered around query handling.
- RCE is restricted to the specific places where user input is ingested and executed as code.
In each of these cases, the issue is entirely contained within the application, and the prevention strategy is direct and surgical. Want to run user code? Add a sandbox or use a custom DSL. Want to stop XSS? Ensure user input is a string.
Vulnerabilities in these classes target a single layer: direct bypasses of the prevention mechanism. For example, in this RCE that Tachyon found in AutoGPT, the disabled parameter was implemented incorrectly, leading to an unexpected RCE surface. The fix was obvious: patch disabled.
SSRF, on the other hand, crosses many layers at once:
- URL parsing: what does this host actually point to?
- DNS: what IP does that host resolve to right now?
- Redirects: where does hop 2 or 3 go?
- IP policy: is that destination private, link-local, loopback, or metadata?
- Network controls: can this workload reach that destination anyway?
Vulnerabilities arise from any of these layers:
- You can obfuscate hosts in various ways to bypass blocklists, or point to local addresses in non-obvious ways.
- You can use DNS rebinding, where an attacker hosts their own DNS server to return different IPs at time-of-check and time-of-use.
- You can exploit redirect chains to launder a benign-looking initial URL through one or more hops that eventually land on an internal or restricted endpoint.
- You can bounce your request across multiple internal services until you find one with incorrect egress settings.
Almost none of these are localized within the application. The developer needs a complete view of the infrastructure to design comprehensive SSRF prevention. Most developers don't have this, and so, their fixes are insufficient.
Why This Is Extra Painful in Python
After many attempts to properly fix this in our codebase, one thing is clear: Python is especially bad for SSRFs. The two most common HTTP clients, requests and httpx are excellent transport libraries, but they do not provide a complete safe-by-default mode for untrusted URLs. By default, requests.get will:
- send requests to loopback or link-local.
- follow any number of redirects.
- not re-validate the destination IP after following redirects — so a redirect from a public host to an internal one sails through.
- accept URL schemes beyond
http/httpsif a transport adapter is registered (file://is a classic footgun in adjacent libraries). - not enforce any timeout by default, opening the door to DNS rebinding attacks where the attacker's server stalls just long enough for the cached DNS entry to expire and re-resolve to a different IP.
httpx is better, but not much better. It still won't validate destination IPs against private ranges or re-check after redirects, and it doesn't handle DNS rebinding.
More generally, neither library treats "this URL came from an untrusted user" as a first-class concern. That responsibility falls entirely on the application developer, each of whom has to build their own SSRF guardrails. This involves:
- Custom preflight URL checks.
- Custom redirect handling rules.
- Custom connect-time IP policy.
- Custom egress restrictions outside the app.
This repeated custom work has to be reimplemented each time, without any errors. This is pretty difficult! And it means that SSRF "fixes" rarely merit their namesake.
This is why we built Drawbridge. But before we get to our solution, let's talk about what "correct" actually looks like.
The Right Way to Handle SSRF
The absolute best way to fix SSRF is to define it out of existence. Most features do not actually need the functionality of: "the server should fetch any URL on the internet." If you remove that capability, then you remove the entire class of issues.
You should prefer product designs like:
- Accept provider IDs or object IDs over URLs.
- Use explicit integrations with known endpoints instead of free-form URL input.
- Let users upload content directly rather than asking your server to fetch it.
- Download URLs on the client and pass the contents to the server.
But sometimes, you do genuinely need your backend to fetch a user-supplied URL. You can't build a webhook, or link previews, or import-from-website features, without it. In those cases, the goal shifts from eliminating the fetch to constraining it.
The baseline policy for untrusted fetches should be:
- Provide a sane policy of URL behavior. Allow only
http/https, limit ports (typically80/443), cap redirects, block private/link-local/loopback/metadata destinations, and disable forwarding sensitive credentials across origins. - Re-check every redirect hop. Either disable redirects for untrusted fetches or re-validate each redirect destination before following it.
- Block internal ranges at the network layer. Use outbound ACLs, proxy controls, or firewall rules so app-level mistakes cannot reach sensitive internal ranges.
- Harden cloud metadata and identity endpoints. On AWS, use IMDSv2 (metadata mode requiring session tokens) and disable IMDS where possible. Similar metadata hardening guidance exists for GCP and Azure. Enforce deny rules for metadata and workload-identity endpoints at the network layer as well.
- Add telemetry for outbound request anomalies. Log destination host/IP, redirect chains, and reject reasons. SSRF attempts are often visible first as unusual outbound traffic behavior.
This is a complex set of requirements, and implementing them correctly every time, across every service, is harder than it looks. One missed edge case and attackers are in.
We found ourselves duplicating these protections in Tachyon over and over again, and that led us to look for a Python library that handles these. As far as I can tell, this doesn't exist.
That's what led us to build Drawbridge: a drop-in httpx-compatible layer that enforces SSRF policy at the transport level. It handles URL validation, redirect re-checking, IP range blocking, and the other constraints described above so you don't have to reimplement them from scratch in every service. It's not a silver bullet, but it makes the default path the safe path.
We don't reimplement cryptography each time we want to build authentication. Why should we reimplement SSRF protection?