Server-Side Request Forgery (SSRF)
Server-Side Request Forgery (SSRF)
What it is
CWE-918 exists when an application fetches a URL supplied (directly or indirectly) by an attacker, without sufficient validation of the destination. An attacker uses this to probe internal services, exfiltrate cloud-metadata credentials (AWS IMDS, GCP metadata), reach databases or admin panels not exposed to the internet, or proxy traffic through the application.
Why it matters
SSRF was the root cause of the 2019 Capital One breach (100 million customer records exfiltrated via AWS IMDS abuse). It is in the OWASP Top 10 as A10:2021. Modern cloud environments make SSRF especially dangerous because instance-metadata services accept un-authenticated local-network requests and return IAM credentials that are valid for the duration of the instance.
Common patterns
- •User-supplied URLs passed to fetch, axios, requests.get, urllib.urlopen, http.Client.Do.
- •Image, document, or video preview features that fetch a URL the user provides.
- •Webhook delivery to user-configured destinations without allowlist validation.
- •Server-side OAuth or OpenID-Connect redirect_uri handling that fetches the discovery document.
- •PDF generation or screenshot services where the user supplies the URL to render.
Languages affected
What Deva detects
Deva flags fetch, axios.get/post/request, requests.get, urllib.request.urlopen, http.Client.Do, java.net.URL.openConnection, RestTemplate.exchange, and similar calls when the URL argument traces to a user-controlled source without passing through a recognized URL-allowlist or DNS-rebinding-resistant resolver. The scanner specifically calls out IMDS-style endpoints (169.254.169.254, metadata.google.internal) in fix recommendations.
Example
Vulnerable
app.get('/preview', async (req, res) => {
const response = await fetch(req.query.url)
res.send(await response.text())
})Fixed
import { URL } from 'url'
import dns from 'dns/promises'
import net from 'net'
const ALLOWED_HOSTS = new Set(['example.com', 'images.example.com'])
async function isPublicAddress(hostname) {
const records = await dns.lookup(hostname, { all: true })
return records.every(({ address }) => {
if (net.isIPv4(address)) {
const parts = address.split('.').map(Number)
// Block 10/8, 172.16/12, 192.168/16, 127/8, 169.254/16
if (parts[0] === 10) return false
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return false
if (parts[0] === 192 && parts[1] === 168) return false
if (parts[0] === 127) return false
if (parts[0] === 169 && parts[1] === 254) return false
}
return true
})
}
app.get('/preview', async (req, res) => {
let target
try { target = new URL(req.query.url) } catch { return res.status(400).send('Bad URL') }
if (!['http:', 'https:'].includes(target.protocol)) return res.status(400).send('Bad protocol')
if (!ALLOWED_HOSTS.has(target.hostname)) return res.status(403).send('Host not allowed')
if (!(await isPublicAddress(target.hostname))) return res.status(403).send('Private IP')
const response = await fetch(target.toString())
res.send(await response.text())
})Explanation
The vulnerable version fetches whatever URL the user supplies. An attacker pointing it at http://169.254.169.254/latest/meta-data/iam/security-credentials/ exfiltrates AWS IAM credentials. The fix combines four defenses: protocol allowlist (block file:// and gopher://), hostname allowlist (limit destinations), DNS resolution check against private-network ranges (prevent rebinding to internal IPs), and the IMDS-specific 169.254/16 block. Production deployments should also use a dedicated egress proxy with explicit allow-rules.
Where this fits in OWASP Top 10
Compliance framework mapping
| Framework | Controls |
|---|---|
| OWASP Top 10 (2021) | A10:2021 Server-Side Request Forgery |
| NIST 800-53 Rev 5 | SC-7 Boundary ProtectionSI-10 Information Input Validation |
| CMMC 2.0 L2 | SC.L2-3.13.1 Communications protection |
Related CWEs
Deva detects CWE-918 alongside 970+ other CWE patterns at write time, with AI-assisted fix generation that maintains compliance.