The project is in a healthy, maintained state
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 the request to proceed.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 1.0, < 3
 Project Readme

Faraday SSRF Filter

Gem Version

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/https by default)
  • Works with all Faraday adapters

Installation

Add to your Gemfile:

gem 'faraday-ssrf-filter'

Or install directly:

gem install faraday-ssrf-filter

Usage

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::PrivateIPError

Configuration 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
end

Error 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::SSRFError => e
  # Catch-all for any SSRF error
end

Blocked 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

  1. Scheme validation — Only http and https are allowed by default
  2. Direct IP blocking — URLs with IP addresses instead of hostnames are blocked by default
  3. DNS resolution — The hostname is resolved to IP addresses using Resolv.getaddresses
  4. IP validation — Each resolved IP is checked against the comprehensive denylist
  5. Hostname replacement (HTTP) — For HTTP requests, the URL hostname is replaced with the validated IP and the Host header is set to the original hostname, preventing DNS rebinding
  6. TLS preservation (HTTPS) — For HTTPS requests, the hostname is preserved in the URL to maintain correct TLS SNI and certificate verification. The resolved IP is stored in the X-Faraday-SSRF-Resolved-IP header
  7. Redirect validation — Redirect responses (3xx with Location header) are inspected. The redirect target is resolved and validated against the same denylist, raising UnsafeRedirectError if 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
end

The SSRF filter also validates redirect Location headers in responses as defense-in-depth, regardless of middleware ordering.

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.