guest@websec ~ % man 13-ssrf
13 · Server-Side Request Forgery
Any feature where our server fetches a user's URL (avatar from link, webhook, link preview, PDF render) can be turned against us. Attackers can't reach our internal network. Our server can, and SSRF makes it fetch for them.
What attackers aim for
- Cloud metadata (
169.254.169.254) hands out the server's cloud credentials. This was behind the 2019 Capital One breach. - Internal services that trust anything on the private network.
- localhost services on the same box.
Why simple blocklists fail
Blocking "localhost" in the string doesn't work. IPs can be written many ways, hostnames can point inside, and redirects can lead inside. Try it:
try:
| check | decision | what actually happens |
|---|
Defenses
- Avoid it if you can: take uploads instead, or only fetch from known hosts.
- Otherwise: resolve the hostname yourself, block private IPs, then connect to that exact IP.
- Don't follow redirects blindly.
- Short timeouts, and never echo the response back.
- On AWS, require IMDSv2 so a plain GET can't read metadata.
Rails does nothing here. Watch for
URI.open(params[…]), HTTP clients with user URLs, and attach(io: URI.open(url)). The ssrf_filter gem does the hard part.# Gemfile: gem "ssrf_filter" def import_avatar(url) uri = URI.parse(url) raise ArgumentError, "http(s) only" unless %w[http https].include?(uri.scheme) response = SsrfFilter.get(uri, max_redirects: 0) # resolves, blocks private IPs, pins IP raise "not an image" unless response["content-type"].to_s.start_with?("image/") avatar.attach(io: StringIO.new(response.body), filename: "avatar") rescue SsrfFilter::Error, URI::InvalidURIError errors.add(:avatar_url, "can't be fetched") # generic message, no body echo end avatar.attach(io: URI.open(params[:avatar_url]), filename: "a") # SSRF
// Node: request-filtering-agent blocks private IPs after DNS resolution. // It needs an agent-aware client (node-fetch, axios, got): Node's built-in fetch ignores `agent`. import fetch from "node-fetch"; import { useAgent } from "request-filtering-agent"; const res = await fetch(url, { agent: useAgent(url), redirect: "manual" }); await fetch(req.body.url); // SSRF