Skip to content

Commit bdeed01

Browse files
committed
Introduce rack.request.trusted_proxy.
1 parent 75e2823 commit bdeed01

File tree

2 files changed

+127
-1
lines changed

2 files changed

+127
-1
lines changed

lib/rack/request.rb

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require 'ipaddr'
34
require_relative 'constants'
45
require_relative 'utils'
56
require_relative 'media_type'
@@ -588,11 +589,40 @@ def accept_language
588589
end
589590

590591
def trusted_proxy?(ip)
591-
Rack::Request.ip_filter.call(ip)
592+
case get_header('rack.request.trusted_proxy')
593+
when nil
594+
# Default to class-level ip_filter:
595+
Rack::Request.ip_filter.call(ip)
596+
when true
597+
# Trust all proxies:
598+
true
599+
when false
600+
# Trust no proxies:
601+
false
602+
when Array
603+
# Trust only specified IPs/ranges:
604+
get_header('rack.request.trusted_proxy').any? do |pattern|
605+
ip_matches?(ip, pattern)
606+
end
607+
end
592608
end
593609

594610
private
595611

612+
# Check if an IP address matches a pattern (IP address or CIDR range).
613+
def ip_matches?(ip, pattern)
614+
# Direct string match for exact IP comparison:
615+
return true if ip == pattern
616+
617+
begin
618+
ip_addr = IPAddr.new(ip)
619+
pattern_addr = IPAddr.new(pattern)
620+
pattern_addr.include?(ip_addr)
621+
rescue IPAddr::InvalidAddressError
622+
false
623+
end
624+
end
625+
596626
def default_session; {}; end
597627

598628
# Assist with compatibility when processing `X-Forwarded-For`.

test/spec_request.rb

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1785,6 +1785,102 @@ def ip_app
17851785
req.trusted_proxy?("2001:470:1f0b:18f8::1").must_equal false
17861786
end
17871787

1788+
it "uses rack.request.trusted_proxy env key when set to nil (default behavior)" do
1789+
# When nil, should fall back to ip_filter
1790+
env = Rack::MockRequest.env_for("/")
1791+
env['rack.request.trusted_proxy'] = nil
1792+
req = make_request(env)
1793+
1794+
req.trusted_proxy?('127.0.0.1').must_equal true
1795+
req.trusted_proxy?('10.0.0.1').must_equal true
1796+
req.trusted_proxy?('192.168.1.1').must_equal true
1797+
req.trusted_proxy?('1.2.3.4').must_equal false
1798+
end
1799+
1800+
it "trusts all proxies when rack.request.trusted_proxy is true" do
1801+
env = Rack::MockRequest.env_for("/")
1802+
env['rack.request.trusted_proxy'] = true
1803+
req = make_request(env)
1804+
1805+
req.trusted_proxy?('127.0.0.1').must_equal true
1806+
req.trusted_proxy?('1.2.3.4').must_equal true
1807+
req.trusted_proxy?('8.8.8.8').must_equal true
1808+
req.trusted_proxy?('2001:470:1f0b:18f8::1').must_equal true
1809+
end
1810+
1811+
it "trusts no proxies when rack.request.trusted_proxy is false" do
1812+
env = Rack::MockRequest.env_for("/")
1813+
env['rack.request.trusted_proxy'] = false
1814+
req = make_request(env)
1815+
1816+
req.trusted_proxy?('127.0.0.1').must_equal false
1817+
req.trusted_proxy?('10.0.0.1').must_equal false
1818+
req.trusted_proxy?('192.168.1.1').must_equal false
1819+
req.trusted_proxy?('1.2.3.4').must_equal false
1820+
end
1821+
1822+
it "trusts only specified IPs when rack.request.trusted_proxy is an array" do
1823+
env = Rack::MockRequest.env_for("/")
1824+
env['rack.request.trusted_proxy'] = ['10.0.0.1', '192.168.1.100']
1825+
req = make_request(env)
1826+
1827+
req.trusted_proxy?('10.0.0.1').must_equal true
1828+
req.trusted_proxy?('192.168.1.100').must_equal true
1829+
req.trusted_proxy?('10.0.0.2').must_equal false
1830+
req.trusted_proxy?('192.168.1.101').must_equal false
1831+
req.trusted_proxy?('127.0.0.1').must_equal false
1832+
end
1833+
1834+
it "supports CIDR ranges in rack.request.trusted_proxy array" do
1835+
env = Rack::MockRequest.env_for("/")
1836+
env['rack.request.trusted_proxy'] = ['10.0.0.0/24', '192.168.1.0/28']
1837+
req = make_request(env)
1838+
1839+
# 10.0.0.0/24 covers 10.0.0.0 - 10.0.0.255
1840+
req.trusted_proxy?('10.0.0.1').must_equal true
1841+
req.trusted_proxy?('10.0.0.100').must_equal true
1842+
req.trusted_proxy?('10.0.0.255').must_equal true
1843+
req.trusted_proxy?('10.0.1.1').must_equal false
1844+
1845+
# 192.168.1.0/28 covers 192.168.1.0 - 192.168.1.15
1846+
req.trusted_proxy?('192.168.1.5').must_equal true
1847+
req.trusted_proxy?('192.168.1.15').must_equal true
1848+
req.trusted_proxy?('192.168.1.16').must_equal false
1849+
end
1850+
1851+
it "supports IPv6 addresses in rack.request.trusted_proxy array" do
1852+
env = Rack::MockRequest.env_for("/")
1853+
env['rack.request.trusted_proxy'] = ['2001:db8::1', 'fd00::/8']
1854+
req = make_request(env)
1855+
1856+
req.trusted_proxy?('2001:db8::1').must_equal true
1857+
req.trusted_proxy?('2001:db8::2').must_equal false
1858+
req.trusted_proxy?('fd00::1').must_equal true
1859+
req.trusted_proxy?('fd00::ffff').must_equal true
1860+
req.trusted_proxy?('fe00::1').must_equal false
1861+
end
1862+
1863+
it "handles invalid IP addresses gracefully in rack.request.trusted_proxy" do
1864+
env = Rack::MockRequest.env_for("/")
1865+
env['rack.request.trusted_proxy'] = ['10.0.0.1', 'invalid-ip']
1866+
req = make_request(env)
1867+
1868+
req.trusted_proxy?('10.0.0.1').must_equal true
1869+
req.trusted_proxy?('invalid-ip').must_equal true # Direct string match
1870+
req.trusted_proxy?('192.168.1.1').must_equal false
1871+
end
1872+
1873+
it "can use Rack::Config to set rack.request.trusted_proxy" do
1874+
app = lambda { |env| [200, {}, [Rack::Request.new(env).trusted_proxy?('8.8.8.8').to_s]] }
1875+
config_app = Rack::Config.new(app) do |env|
1876+
env['rack.request.trusted_proxy'] = true
1877+
end
1878+
1879+
mock = Rack::MockRequest.new(config_app)
1880+
res = mock.get '/'
1881+
res.body.must_equal 'true'
1882+
end
1883+
17881884
it "sets the default session to an empty hash" do
17891885
req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
17901886
session = req.session

0 commit comments

Comments
 (0)