Faraday SSRF Filter
A Faraday middleware that prevents Server-Side Request Forgery (SSRF) attacks by validating resolved IP addresses against known private and reserved IP ranges before allowing requests to proceed.
Inspired by ssrf_filter (which uses Net::HTTP directly), this gem brings the same level of SSRF protection to any Faraday-based HTTP client.
Features
- Blocks requests to all private/reserved IPv4 and IPv6 ranges (RFC 1918, RFC 6598, loopback, link-local, multicast, etc.)
- Detects IPv4-mapped/compatible/translated IPv6 addresses (e.g.,
::ffff:127.0.0.1) - Detects NAT64 well-known prefix addresses (e.g.,
64:ff9b::10.0.0.1) - DNS resolution with IP pinning to prevent DNS rebinding attacks
- Blocks direct IP address usage by default
- Configurable allowlist/denylist for fine-grained control
- Custom DNS resolver support
-
Scheme validation (only
http/httpsby default) - Works with all Faraday adapters (HTTPS connect-time pinning requires the default
Net::HTTPadapter)
Requirements
- Ruby 3.3+
- Faraday 1.x or 2.x
Installation
Add to your Gemfile:
gem 'faraday-ssrf-filter'Or install directly:
gem install faraday-ssrf-filterUsage
Basic Usage
require 'faraday/ssrf_filter'
conn = Faraday.new(url: 'https://api.example.com') do |f|
f.request :ssrf_filter
f.adapter Faraday.default_adapter
end
# Safe requests work normally
response = conn.get('/data')
# Requests to private IPs are blocked
# e.g., if evil.com resolves to 127.0.0.1
# => raises Faraday::SsrfFilter::PrivateIPErrorConfiguration Options
conn = Faraday.new(url: 'https://api.example.com') do |f|
f.request :ssrf_filter,
# Allow specific private ranges (e.g., internal services)
allowlist: ['10.0.0.0/8'],
# Block specific public ranges
denylist: ['93.184.216.0/24'],
# Allow direct IP addresses in URLs (default: false)
allow_ip_addresses: true,
# Restrict allowed URI schemes (default: ['http', 'https'])
allowed_schemes: %w[http https],
# Custom DNS resolver (default: Resolv.getaddresses)
resolver: ->(hostname) { Resolv.getaddresses(hostname) }
f.adapter Faraday.default_adapter
endError Handling
All errors inherit from Faraday::SsrfFilter::SSRFError (which inherits from Faraday::Error):
begin
conn.get('/data')
rescue Faraday::SsrfFilter::PrivateIPError => e
# Hostname resolved to a private/reserved IP
rescue Faraday::SsrfFilter::DirectIPError => e
# URL contains a direct IP address (blocked by default)
rescue Faraday::SsrfFilter::InvalidSchemeError => e
# URI scheme not in allowed list
rescue Faraday::SsrfFilter::DNSResolutionError => e
# Could not resolve hostname
rescue Faraday::SsrfFilter::UnsafeRedirectError => e
# Response redirects to a private/reserved IP or disallowed scheme
rescue Faraday::SsrfFilter::MiddlewareOrderError => e
# A redirect-following middleware is registered inside the filter
rescue Faraday::SsrfFilter::SSRFError => e
# Catch-all for any SSRF error
endBlocked IP Ranges
IPv4
| Range | Purpose |
|---|---|
0.0.0.0/8 |
Current network |
10.0.0.0/8 |
Private (RFC 1918) |
100.64.0.0/10 |
Carrier-grade NAT (RFC 6598) |
127.0.0.0/8 |
Loopback |
169.254.0.0/16 |
Link-local (includes cloud metadata endpoints) |
172.16.0.0/12 |
Private (RFC 1918) |
192.0.0.0/24 |
IETF protocol assignments |
192.0.2.0/24 |
TEST-NET-1 |
192.168.0.0/16 |
Private (RFC 1918) |
198.18.0.0/15 |
Benchmarking |
198.51.100.0/24 |
TEST-NET-2 |
203.0.113.0/24 |
TEST-NET-3 |
224.0.0.0/4 |
Multicast |
240.0.0.0/4 |
Reserved |
255.255.255.255/32 |
Broadcast |
IPv6
| Range | Purpose |
|---|---|
::/128 |
Unspecified |
::1/128 |
Loopback |
100::/64 |
Discard prefix |
2001::/32 |
Teredo tunneling |
2001:2::/48 |
Benchmarking |
2001:10::/28 |
ORCHID |
2001:20::/28 |
ORCHIDv2 |
2001:db8::/32 |
Documentation |
2002::/16 |
6to4 tunneling |
3fff::/20 |
Documentation |
5f00::/16 |
Segment Routing (SRv6) |
fc00::/7 |
Unique local |
fe80::/10 |
Link-local |
ff00::/8 |
Multicast |
64:ff9b:1::/48 |
NAT64 local prefix |
Additionally, all IPv4 blacklisted ranges are also blocked in their IPv4-compatible (::x.x.x.x), IPv4-mapped (::ffff:x.x.x.x), IPv4-translated (::ffff:0:x.x.x.x), and NAT64 (64:ff9b::x.x.x.x) IPv6 representations.
How It Works
-
Scheme validation — Only
httpandhttpsare allowed by default - Direct IP blocking — URLs with IP addresses instead of hostnames are blocked by default
-
DNS resolution — The hostname is resolved to IP addresses using
Resolv.getaddresses - IP validation — Each resolved IP is checked against the comprehensive denylist
-
Hostname replacement (HTTP) — For HTTP requests, the URL hostname is replaced with the validated IP and the
Hostheader is set to the original hostname, preventing DNS rebinding -
Connect-time pinning (HTTPS) — For HTTPS requests, the hostname is preserved in the URL to maintain correct TLS SNI and certificate verification. With the
Net::HTTPadapter (Faraday's default), the connection is opened to the validated IP viaNet::HTTP#ipaddr=, so a second DNS lookup cannot rebind the request to a private address. Through an HTTP proxy, theCONNECTrequest also targets the validated IP. Other adapters, andNet::HTTPthrough a SOCKS proxy, resolve the hostname again when connecting, so those HTTPS requests are validated but not pinned. The resolved IP is stored in theX-Faraday-SSRF-Resolved-IPheader -
Redirect validation — Redirect responses (3xx with
Locationheader) are inspected. The redirect target is resolved and validated against the same denylist, raisingUnsafeRedirectErrorif it points to a private/reserved IP
Middleware Ordering
When using a redirect-following middleware (e.g., faraday-follow_redirects), place it before the SSRF filter so each redirect is validated:
conn = Faraday.new(url: 'https://api.example.com') do |f|
f.response :follow_redirects # outer — follows redirects
f.request :ssrf_filter # inner — validates each request including redirects
f.adapter Faraday.default_adapter
endThe filter only validates a redirect Location when the 3xx response passes back through it. A redirect middleware registered after the filter follows each hop itself, so the filter never sees the intermediate 3xx and redirect targets are not validated. To prevent that silent bypass, the filter raises Faraday::SsrfFilter::MiddlewareOrderError on the first request when it detects faraday-follow_redirects or faraday_middleware's FollowRedirects inside it.
On a redirect to a different host, the Host header pinned for the previous HTTP hop is dropped before the next hop is validated and pinned, so it never reaches the new host.
Acknowledgments
This gem is heavily inspired by ssrf_filter by Arkadiy Tetelman. The comprehensive IP blacklist, IPv4-mapped IPv6 detection, and NAT64 handling are all based on his excellent work. Thank you for building such a thorough and well-tested SSRF protection library for the Ruby ecosystem.
License
MIT License. See LICENSE for details.