An approachable way to think about SSRF
Imagine your web server as a helpful assistant that can reach out to other services — download an image, check a URL, or fetch data for a preview. Server-Side Request Forgery, or SSRF, is when an attacker convinces that assistant to reach places it shouldn't, using inputs your application accepts. The key problem is trust: systems inside your network often treat requests coming from your server as trusted.
Why SSRF matters
SSRF is dangerous because it lets an external actor misuse the server's network access. That can mean quietly reading internal services, querying cloud metadata endpoints for credentials, or triggering administrative endpoints that aren't exposed publicly. In other words, an SSRF can turn a seemingly harmless feature — like fetching an avatar image or previewing a web page — into a window into your private infrastructure.
How SSRF typically appears in real applications
You will most often find SSRF where your application accepts a URL or path from a user and then performs a server-side fetch. Common examples are site previewers, import tools that fetch RSS or feeds, screenshot or PDF rendering services, and proxy endpoints. What makes these features convenient — the server doing the heavy lifting — also makes them risky when input is not constrained.
Sometimes the vulnerability is obvious because the server returns the fetched content to the attacker. Other times it is "blind": the attacker receives no direct response but can still confirm that the server performed the request by observing side effects such as DNS lookups or timing differences. Both forms are valuable to attackers for different purposes.
What usually goes wrong
A typical mistake is trusting a user-supplied value and passing it straight into a networking function. For instance, an endpoint that reads a `?url=` parameter and calls a simple fetch or file-read function without any validation can be redirected to internal IPs, cloud metadata endpoints, or services running on localhost. Other mistakes include following redirects without checking final destinations, allowing multiple protocols (like file:// or gopher://), and failing to normalize hostnames or IP representations.
Practical defenses — philosophy first
The most reliable approach is architectural: remove the need for arbitrary server-side fetches where possible. When that isn't possible, apply strict controls. Prefer an allowlist of approved hosts or domains and run network-level protections such as egress firewalls and proxies that enforce destination checks. Treat WAFs and input filters as useful but not sufficient on their own.
Other defensive ideas that pair well together are conservative URL parsing, rejecting direct IP addresses unless explicitly required, limiting redirects and request timeouts, and logging every outbound request so you can detect anomalies quickly.
Short, defensive code examples
Below are compact examples that show a safer mindset. They are intentionally conservative: validate, then act.
PHP — validate and allowlist
<?php
$allowedHosts = ['example.com', 'images.example.com'];
$url = filter_input(INPUT_GET, 'url', FILTER_VALIDATE_URL);
if (!$url) {
http_response_code(400);
echo "Invalid URL";
exit;
}
$host = parse_url($url, PHP_URL_HOST);
if (!in_array($host, $allowedHosts, true)) {
http_response_code(403);
echo "URL not permitted";
exit;
}
// Keep timeouts low and avoid following redirects blindly
$opts = ["http" => ["method" => "GET", "timeout" => 5]];
$context = stream_context_create($opts);
$content = @file_get_contents($url, false, $context);
if ($content === false) {
http_response_code(502);
echo "Failed to fetch";
exit;
}
echo $content;
?>
Node.js (Express) — parse and enforce
const { URL } = require('url');
const allowedHosts = new Set(['example.com']);
app.get('/fetch', async (req, res) => {
try {
const url = new URL(req.query.url);
if (!allowedHosts.has(url.hostname)) return res.status(403).send('Not allowed');
const resp = await fetch(url.toString(), { redirect: 'manual', signal: AbortSignal.timeout(5000) });
if (!resp.ok) return res.status(502).send('Bad gateway');
const body = await resp.text();
res.send(body);
} catch (err) {
res.status(400).send('Invalid URL or fetch error');
}
});
Python — parse and restrict
from urllib.parse import urlparse
import requests
ALLOWED_HOSTS = {'example.com'}
def fetch(url):
parsed = urlparse(url)
if parsed.hostname not in ALLOWED_HOSTS:
raise ValueError('Host not allowed')
resp = requests.get(url, timeout=5, allow_redirects=False)
resp.raise_for_status()
return resp.text
These snippets illustrate the pattern: do not assume the user input is safe; parse it, check the host against an allowlist, set timeouts, and avoid following redirects automatically.
Network controls and architecture
Even the best code checks can be bypassed if the server is allowed to reach sensitive internal endpoints. Use egress filtering to block private IP ranges from application hosts, and consider routing all outbound traffic through a proxy that enforces destination policies. In cloud environments, harden instance metadata services (for example, require IMDSv2 on AWS) and segment administrative interfaces behind strict ACLs.
Detecting SSRF attempts
Logging is your first line of detection. Record outbound requests including hostname, IP (when resolvable), the initiating user or request ID, and timestamps. Watch for requests to private IP ranges, to cloud metadata endpoints, or a sudden spike in outbound lookups from a single account. IDS/IPS and behavior-based alerting can help catch blind SSRF attempts that do not return content to the attacker.
Common pitfalls worth remembering
Attackers often rely on subtle encoding tricks: DNS rebinding, numeric IP encodings, IPv6 forms, and redirects can all bypass naive checks. Treat URL parsing and name resolution carefully: normalize inputs, check final IP addresses before connecting, and be skeptical of any feature that allows open network destinations.
Final thoughts
SSRF is less about a single bug and more about a lapse in boundary enforcement. When your server can act on untrusted input by reaching other systems, that trust needs to be enforced through both code and network configuration. Remove unnecessary features, validate inputs conservatively, and assume your server's network access is sensitive.
For practical projects, visit my developer portfolio.