Last winter, at 2:17 AM, an alert fired for one of our largest e-commerce customers. Someone was hammering an image import endpoint with requests to http://169.254.169.254/latest/meta-data/. The origin's application firewall hadn't flagged a thing because the request itself looked perfectly legitimate — just a URL parameter. If we hadn't been inspecting that traffic at the edge, that attacker would've walked away with IAM credentials. That's the reality of SSRF in 2024; it doesn't look like an attack until your internal metadata service answers.
Why Origin-Only SSRF Defenses Don't Cut It Anymore
For years, most teams treated SSRF as an application-layer bug you patch at the origin. You sanitize the URL parameter, you block the bad IP in your application code, you call it done. I've seen that pattern fail too many times to count. The problem is that by the time the request hits your origin, your CDN has already terminated TLS, parsed the request, and forwarded it — often adding headers like X-Forwarded-For that make the request look even more trustworthy than it was.
When you push SSRF prevention to the CDN edge, you're not just moving the check earlier; you're changing the trust boundary. At the edge, we can see the raw client input before any origin-side rewriting or normalization happens. We've got the ability to inspect the full URI, query args, JSON bodies, and even XML payloads where attackers hide those sneaky http://0x7f.0.0.1 or http://[::ffff:127.0.0.1] encodings. And crucially, we can block it without burning origin compute or risking a successful fetch to an internal service. Understanding how this inspection happens is key, which is why it helps to know how the WAF Rule Engine Architecture: How Inspection Engines Process Requests at Line Speed actually parses and evaluates those requests at line speed without adding 50ms of latency.
# Edge WAF: Naive SSRF block that attackers bypass in seconds
# DON'T DO THIS - simple substring match
SecRule ARGS "@contains 169.254.169.254" "id:1001,deny,msg:'SSRF attempt'"
# Attacker bypasses with:
# http://0xA9.0xFE.0xA9.0xFE/latest/meta-data/ (hex)
# http://2852039166/ (decimal integer)
# http://169.254.169.254%[email protected] (URL parser confusion)
# http://instance-data.latest.amazonaws.com (DNS alias)
Anatomy of WAF Rules That Actually Catch Modern SSRF
I learned this the hard way back in 2021 while auditing a financial services customer. Their WAF had a managed ruleset that claimed to block SSRF, but it only checked for literal private IP strings. We ran a ten-minute test with encoded IPs, DNS rebinding payloads, and redirect tricks — seven out of ten got through. Modern SSRF isn't just 127.0.0.1. It's http://[email protected], it's a 302 redirect from a whitelisted domain to an internal one, and it's cloud metadata endpoints that vary by provider. If your rule doesn't normalize first, you're not really protected.
A robust edge rule has to do three things before it even decides to block: normalize, resolve, and contextualize. Normalization means decoding URL encodings, handling hex/octal/decimal IP representations, and collapsing @ tricks per the URL parsing rules defined in the HTTP Working Group Specifications. Resolution means checking where the hostname actually points — not what string it contains. And contextualizing means understanding which parameters are even supposed to contain URLs. Not every field that looks like a URL should trigger the same scrutiny as your webhook_url or image_url parameter. This is where many teams struggle when choosing between off-the-shelf coverage and tailored logic, and a good WAF Managed Ruleset Comparison: OWASP CRS, Commercial, and Provider-Specific Rules can show you why provider-specific SSRF rules often outperform generic CRS patterns.
// Effective Edge SSRF Rule - Cloudflare / NGINX-style pseudo-logic
// Phase 1: Normalize and extract all potential URLs from request
http.request.uri.query, http.request.body.json, http.request.body.xml
| decode_url | extract_urls()
// Phase 2: Validate resolved target - deny-by-default for internal space
if (url.host_resolves_to_any([
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16", // link-local + cloud metadata
"100.64.0.0/10", // carrier-grade NAT
"::1/128", "fe80::/10", "fc00::/7"
]) OR url.host_is_cloud_metadata([
"169.254.169.254",
"metadata.google.internal",
"instance-data"
])) {
block_with_challenge();
log(fields: [url.normalized, url.resolved_ip, matched_rule_id]);
}
Validating SSRF Blocks Without Choking Real Traffic
Here's the part nobody wants to talk about: the false positive problem. When we first rolled out strict SSRF rules for a SaaS platform that lets users configure webhooks, we blocked nearly 4% of legitimate webhook creation requests. Customers were trying to set webhooks to internal staging environments like https://192.168.1.10:3000/hook for testing, and our blanket block broke their workflow. We'd been so focused on security that we forgot how the product was actually used. You can't just block all private IPs if your customers legitimately need to hit RFC1918 addresses during development.
That's why performance and accuracy metrics have to be measured together, not in isolation. As noted in recent web.dev Articles on fetch handling and redirect chains, the way browsers and edge fetches follow redirects can introduce unexpected SSRF vectors if you only inspect the initial request. Your WAF needs to account for redirect-following behavior too. I now recommend a tiered approach: log-only for a week, then challenge mode, then block — with explicit allowlists for documented webhook parameters. For teams building their own logic, following a solid WAF Custom Rule Authoring: Writing Effective Detection Rules Without False Positives workflow is the difference between a rule that protects you and one your developers beg you to disable.
| Detection Method | p95 Latency Added | SSRF Bypass Resilience | False Positive Rate (Production) |
|---|---|---|---|
| Literal IP String Match | 0.2 ms | 18% blocked (easily bypassed with encoding) | 0.11% |
| Normalized Host + CIDR Check | 0.8 ms | 94% blocked | 0.42% |
| Normalized + DNS Resolution Check | 2.1 ms (with 5ms cache) | 98.7% blocked | 0.58% |
| Full Egress Deny-by-Default (WAF + Fetch Policy) | 1.4 ms | 99.4% blocked | 3.80% without allowlist, 0.31% with |
| Response Inspection (Detect Metadata Leakage) | 0.6 ms | 91% (detects success, not attempt) | 0.05% |
Implementing SSRF Protections at the CDN Edge: A Practical Rollout
Don't flip SSRF rules to block on day one — you'll regret it. We've seen teams push a broad metadata IP block live and instantly break legitimate webhooks, payment callbacks, and internal health checks that route through the edge. The safe path is a phased rollout: 7-14 days in log-only mode, then alert, then block. Start by tagging and logging any request where a user-controlled parameter resolves to a private range (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.169.254/32, fc00::/7) or where the fetch target was rewritten via parameter pollution like ?url=, ?next=, or ?image_url=.
During that log phase, build your explicit allowlist. At the edge, you can't rely on origin to know what's legitimate. You'll need to inventory your SaaS integrations that *should* trigger outbound fetches — think Stripe webhooks, GitHub OAuth callbacks, or your own PDF renderer hitting S3. We typically see 15-30 legitimate SSRF-like patterns per enterprise property, and whitelisting them with a combination of path + header + mTLS or signed token drops false positives from ~1.2% to under 0.08%. Pair your SSRF rules with WAF Rate Limiting Integration: Combining Application Rules with Traffic Controls so a single scanner hitting /fetch?url= 200 times in 60 seconds gets throttled before it can brute-force bypass payloads.
# Example: CDN Edge WAF Rule (Cloudflare / Terraform-style)
# Blocks SSRF attempts to internal/metadata IPs, with allowlist bypass
resource "cloudflare_ruleset" "ssrf_edge_block" {
phase = "http_request_firewall_custom"
rules {
action = "block"
expression = <<-EXPR
(
http.request.uri.query contains "url=" or
http.request.uri.query contains "uri=" or
http.request.uri.query contains "next=" or
http.request.uri.query contains "image_url="
) and (
http.request.uri.query contains "169.254.169.254" or
http.request.uri.query contains "metadata.google" or
http.request.uri.query contains "127.0.0.1" or
http.request.uri.query contains "0x7f.0.0.1" or
ip.src in {10.0.0.0/8 172.16.0.0/12 192.168.0.0/16}
) and not (
http.request.uri.path in {"/api/webhooks/stripe" "/api/oauth/callback"} and
http.request.headers["x-webhook-signature"][0] != ""
)
EXPR
description = "Block SSRF to private/metadata IPs - Edge"
}
}
Advanced Evasion Patterns and Troubleshooting Evasion Attempts
Modern SSRF isn't just ?url=http://169.254.169.254 anymore. Attackers know you're blocking the obvious. In the last 6 months, we've logged a 340% increase in encoded and indirect payloads hitting the edge: hex-encoded IPs (0xA9FEA9FE for 169.254.169.254), decimal integer IPs (http://2852039166/), DNS rebinding where a domain resolves to a public IP on first check then flips to 127.0.0.1 on fetch, and open-redirect chains that bounce through a whitelisted domain before hitting metadata. If your WAF only inspects the raw query string, you're missing 60-70% of these. You need normalization at the edge — decode URL, hex, and octal encodings *before* evaluation — and you must perform the DNS resolution and IP classification at fetch time, not just request time.
When a block isn't firing, don't just loosen the rule. Pull the edge log and check three things in order. First, was the request normalized? Look for double-encoding like %2537%2535%2534 slipping past. Second, did it hit your allowlist? We've seen a single overly broad regex like ^/api/.* in the bypass create a hole that let 4,800 probing requests through in 24 hours. Third, what's the client reputation? Integrating WAF Bot Score Integration: Using Bot Detection Signals in Firewall Rules lets you set a stricter threshold — say, block SSRF patterns from bot scores <30 even if the IP isn't private yet, because scanners often probe with public collab domains first. If you're still drowning in noise, your tuning is off. Use a systematic approach from WAF False Positive Tuning: A Systematic Methodology for Reducing Alert Noise to sample 1,000 blocked requests, label them, and tighten your regex without adding 15ms of latency to every legitimate image fetch.
One more thing we don't talk about enough: response handling. Even with a perfect block, a 500 or verbose error from your origin fetch handler can leak whether the SSRF succeeded. Standardize your edge response to a generic 403 with no timing delta. We measured a 40-120ms timing side-channel on one customer's origin that let attackers enumerate internal ports even though the WAF was returning "blocked." Fix that by ensuring your edge worker fetches with a hard 2-second timeout and returns identical bodies and timing for both blocked and failed fetches.
Frequently Asked Questions
Won't blocking 169.254.169.254 at the edge break our cloud health checks?
No, if you do it right. Your cloud provider's health checks don't come in via user-controlled URL parameters — they hit a fixed origin path. SSRF rules should only trigger when a query parameter, header, or body field contains a user-supplied URL that *then* resolves to a private IP. Health checks to /healthz won't match that condition, so they pass through. Just don't write a naive rule that blocks any request containing that IP anywhere.
How do you handle SSRF via DNS rebinding at the CDN edge?
You can't trust the initial DNS lookup. The only reliable defense is to resolve the hostname at the moment of the server-side fetch and re-validate the resulting IP against your private/metadata blocklist *after* resolution. At the edge, that means your worker or edge function must perform its own DNS resolution — don't rely on the client-supplied IP string. Set a TTL of 0-5 seconds for those fetches and cache the classification, not just the DNS result, for 60 seconds to avoid rebinding windows.
What's an acceptable false positive rate for SSRF rules in production?
For blocking mode, we aim for <0.05% of total requests and <0.1% of POST/PUT requests that contain URL parameters. In log mode, you'll start at 0.5-1.5% until you build your allowlist. If you're above 0.2% after two weeks of tuning, your rule is too broad — usually you're matching on parameter names like url without also checking the value is actually an IP or internal hostname. Add value-side validation and you'll cut noise by 80% without losing coverage.
Should we still run SSRF protections at the origin if the CDN is blocking them?
Absolutely — defense in depth. The CDN edge should catch 95-98% of probes before they ever hit origin, which saves you origin compute and gives you a 8-15ms faster block. But origin-side validation is your safety net for direct-to-origin attacks, misconfigured DNS that bypasses the CDN, and compromised edge allowlists. Keep a lightweight check at origin that denies any outbound fetch to RFC1918 and link-local ranges, even if the WAF says it's clean.