Server-side request forgery happens when your server fetches a URL an attacker supplied, letting them reach anything the server can reach — the cloud metadata endpoint at `169.254.169.254`, internal admin panels, Redis on localhost, or Kubernetes services. String checks on the URL do not work: `localhost` has hundreds of spellings (`127.1`, `0177.0.0.1`, `[::1]`, `2130706433`, a DNS name the attacker points at `127.0.0.1`), and a permitted host can 302-redirect to a forbidden one. The control is to resolve the hostname yourself, reject every address in a private, loopback, link-local or unique-local range, then connect to *that verified IP* rather than re-resolving — and to disable redirect following, or re-run the whole check on each hop. In cloud environments, enforce IMDSv2 and egress rules as well, because the application-level check will eventually be bypassed.
| Languages | Any server that fetches a URL: webhooks, link previews, PDF/image renderers, importers, XML parsers, avatar fetchers |
| Ranges to reject | 127.0.0.0/8, 10/8, 172.16/12, 192.168/16, 169.254/16, 100.64/10, 0.0.0.0/8, ::1, fc00::/7, fe80::/10, and IPv4-mapped IPv6 |
| Key targets | 169.254.169.254 (cloud metadata), metadata.google.internal, localhost services, .svc.cluster.local |
| Bypasses to expect | Decimal and octal IPs, DNS rebinding, 302 redirects, open redirects on an allowlisted host, non-HTTP schemes (file:, gopher:, dict:) |
| Typical impact | Cloud credential theft leading to full account compromise; internal service access; port scanning |
| Not a fix | Rejecting URLs containing 'localhost' or '127.0.0.1', or validating before resolution |
Vulnerable
import requests
def fetch_preview(url: str) -> str:
if "localhost" in url or "127.0.0.1" in url:
raise ValueError("blocked")
return requests.get(url, timeout=5).text
# http://169.254.169.254/latest/meta-data/iam/security-credentials/
# http://2130706433/ (decimal for 127.0.0.1)
# http://attacker.tld/ -> 302 -> http://169.254.169.254/Secure
import ipaddress
import socket
from urllib.parse import urlsplit
import requests
ALLOWED_SCHEMES = {"http", "https"}
ALLOWED_PORTS = {80, 443}
def _resolve_public(host: str) -> str:
"""Every A/AAAA record must be public; return one verified address."""
infos = socket.getaddrinfo(host, None, proto=socket.IPPROTO_TCP)
addresses = {info[4][0] for info in infos}
if not addresses:
raise ValueError("unresolvable host")
for address in addresses:
ip = ipaddress.ip_address(address)
if ip.version == 6 and ip.ipv4_mapped:
ip = ip.ipv4_mapped
if (ip.is_private or ip.is_loopback or ip.is_link_local
or ip.is_reserved or ip.is_multicast or ip.is_unspecified):
raise ValueError(f"blocked address {ip}")
return sorted(addresses)[0]
def fetch_preview(url: str) -> str:
parts = urlsplit(url)
if parts.scheme not in ALLOWED_SCHEMES or not parts.hostname:
raise ValueError("blocked scheme")
if (parts.port or (443 if parts.scheme == "https" else 80)) not in ALLOWED_PORTS:
raise ValueError("blocked port")
verified_ip = _resolve_public(parts.hostname)
# Connect to the address we checked, keeping the Host header and TLS SNI
# intact. Re-resolving here is what DNS rebinding attacks rely on.
target = parts._replace(netloc=verified_ip if parts.port is None
else f"{verified_ip}:{parts.port}").geturl()
response = requests.get(
target,
headers={"Host": parts.netloc},
allow_redirects=False, # re-run the whole check per hop instead
timeout=5,
stream=True,
)
if response.status_code in {301, 302, 303, 307, 308}:
raise ValueError("redirects are not followed")
return response.raw.read(1_000_000, decode_content=True).decode("utf-8", "replace")Checking every resolved address, not just the first, matters: a host with one public and one private A record passes a first-record-only check. Pinning the connection to the verified IP closes the time-of-check/time-of-use gap that DNS rebinding exploits. For TLS, verification still runs against the original hostname via SNI plus the Host header.
Vulnerable
const res = await fetch(req.body.webhookUrl); // follows redirects by defaultSecure
const dns = require("node:dns/promises");
const net = require("node:net");
const ipaddr = require("ipaddr.js");
const BLOCKED = ["unspecified", "private", "loopback", "linkLocal",
"uniqueLocal", "reserved", "carrierGradeNat", "broadcast"];
async function assertPublic(hostname) {
const records = await dns.lookup(hostname, { all: true, verbatim: true });
if (records.length === 0) throw new Error("unresolvable");
for (const { address } of records) {
let ip = ipaddr.parse(address);
if (ip.kind() === "ipv6" && ip.isIPv4MappedAddress()) ip = ip.toIPv4Address();
if (BLOCKED.includes(ip.range())) throw new Error(`blocked ${address}`);
}
return records[0].address;
}
const url = new URL(req.body.webhookUrl);
if (!["http:", "https:"].includes(url.protocol)) throw new Error("blocked scheme");
if (net.isIP(url.hostname) === 0) await assertPublic(url.hostname);
else await assertPublic(url.hostname);
const res = await fetch(url, {
redirect: "manual", // fetch follows redirects unless told not to
signal: AbortSignal.timeout(5000),
});`fetch` follows up to 20 redirects by default, so a permitted host is enough for an attacker who controls it. `redirect: "manual"` makes each hop an explicit decision. Node's `dns.lookup` honours the system resolver, so also apply an egress policy — the application check is one layer.
Each of these is a pull request Orbis AppSec opened against a real open-source repository.
The order-flow service in a Node.js e-commerce backend built an outbound fetch() URL by directly concatenating a configurable `sendingOrder.url` value with a query string, with no validation of protocol or destination. This allowed order data—including customer and payment-adjacent information—to be silently redirected to an attacker-controlled endpoint simply by changing a config value or environment variable.
The `proverbia-scraper.py` script disabled TLS certificate verification on its `requests.get()` call and silenced the resulting security warnings, exposing the scraper to man-in-the-middle attacks. The fix removes the `verify=False` flag and the warning suppression, restoring proper certificate validation while keeping the existing 30-second timeout intact.
A critical Server-Side Request Forgery (SSRF) vulnerability in the ldfetch CLI tool allowed attackers to access internal cloud metadata services and local files through unvalidated URL arguments. The fix introduces strict protocol validation with an explicit opt-in flag for local file access, transforming a dangerous default into a secure-by-design implementation.
A Server-Side Request Forgery (SSRF) vulnerability was discovered in `internal/web/controller/server.go` where the `applySubTemplate` endpoint accepted arbitrary URLs from user input and passed them directly to `serverService.ApplySubTemplateFromGithub()` without any host validation. An attacker could exploit this to make the server issue HTTP requests to internal network resources, cloud metadata endpoints, or redirect-controlled destinations. The fix introduces a strict allowlist that restrict
A permissive semver range in `package.json` allowed npm to install axios versions vulnerable to SSRF (CVE-2024-39338). By bumping the minimum version from `^1.6.0` to `^1.7.4`, all downstream consumers of this SDK are now protected from server-side request forgery attacks. This critical fix required changing just one line in the dependency manifest.
A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in playground.html where the `__forEachRdfMessageChunkFromUrl` function fetched user-controlled URLs without validating against private IP ranges or internal network addresses. The fix introduces a comprehensive `__isBlockedFetchUrl` validation function that blocks requests to localhost, private IP ranges, and link-local addresses before any fetch occurs.
A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in app.py where the `/parse` and `/parse-video` endpoints accepted user-supplied URLs with only substring validation. The application checked if 'doubao.com' appeared anywhere in the URL string, allowing attackers to bypass this check and access internal services, cloud metadata endpoints, or scan the internal network. The fix implemented proper hostname parsing with an allowlist of legitimate domains.
A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `maintenance/getImages.js`, where the `getImage()` function passed database-sourced URLs directly to `axios.get()` without any validation. An attacker who could modify the elements database could redirect these requests to internal network resources — including AWS cloud metadata endpoints — potentially exposing IAM credentials and other sensitive infrastructure data. The fix introduces a strict URL allowlist that limi
The same address has many textual forms — `127.1`, `0177.0.0.1`, `2130706433`, `[::1]`, `[::ffff:127.0.0.1]`, `127.0.0.1.nip.io` — and an attacker can simply register a domain whose A record is `127.0.0.1`. None of those contain the strings you blocked. Resolve the name and judge the resulting address.
The attacker serves a very short TTL and returns a public address to your validation lookup, then a private one to the lookup your HTTP client makes moments later. Validating the IP but then handing the *hostname* to the client is exactly the window it exploits. Connect to the address you verified — pin the IP, keep the Host header — and the second lookup never happens.
It is the strongest application-level control, but two things still break it: an open redirect on an allowlisted domain, and an allowlisted domain whose DNS the attacker can influence. Keep redirect following off, and re-validate after any hop you do allow.
Yes. Blind SSRF still lets an attacker reach internal endpoints and cause side effects — POSTing to an internal admin API, triggering a queue job, or scanning ports by timing the difference between a refused connection and a timeout. Cloud metadata is the common escalation because a single GET returns credentials.
Orbis AppSec scans your GitHub repositories, traces the taint from source to sink, and opens a pull request with the fix applied and verified.
Try Orbis AppSec