1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
|
# frozen_string_literal: true
module HTTPX
class ServerSideRequestForgeryError < Error; end
module Plugins
#
# This plugin adds support for preventing Server-Side Request Forgery attacks.
#
# https://gitlab.com/os85/httpx/wikis/Server-Side-Request-Forgery-Filter
#
module SsrfFilter
module IPAddrExtensions
refine IPAddr do
def prefixlen
mask_addr = @mask_addr
raise "Invalid mask" if mask_addr.zero?
mask_addr >>= 1 while mask_addr.nobits?(0x1)
length = 0
while mask_addr & 0x1 == 0x1
length += 1
mask_addr >>= 1
end
length
end
end
end
using IPAddrExtensions
# https://en.wikipedia.org/wiki/Reserved_IP_addresses
IPV4_BLACKLIST = [
IPAddr.new("0.0.0.0/8"), # Current network (only valid as source address)
IPAddr.new("10.0.0.0/8"), # Private network
IPAddr.new("100.64.0.0/10"), # Shared Address Space
IPAddr.new("127.0.0.0/8"), # Loopback
IPAddr.new("169.254.0.0/16"), # Link-local
IPAddr.new("172.16.0.0/12"), # Private network
IPAddr.new("192.0.0.0/24"), # IETF Protocol Assignments
IPAddr.new("192.0.2.0/24"), # TEST-NET-1, documentation and examples
IPAddr.new("192.88.99.0/24"), # IPv6 to IPv4 relay (includes 2002::/16)
IPAddr.new("192.168.0.0/16"), # Private network
IPAddr.new("198.18.0.0/15"), # Network benchmark tests
IPAddr.new("198.51.100.0/24"), # TEST-NET-2, documentation and examples
IPAddr.new("203.0.113.0/24"), # TEST-NET-3, documentation and examples
IPAddr.new("224.0.0.0/4"), # IP multicast (former Class D network)
IPAddr.new("240.0.0.0/4"), # Reserved (former Class E network)
IPAddr.new("255.255.255.255"), # Broadcast
].freeze
IPV6_BLACKLIST = ([
IPAddr.new("::1/128"), # Loopback
IPAddr.new("64:ff9b::/96"), # IPv4/IPv6 translation (RFC 6052)
IPAddr.new("100::/64"), # Discard prefix (RFC 6666)
IPAddr.new("2001::/32"), # Teredo tunneling
IPAddr.new("2001:10::/28"), # Deprecated (previously ORCHID)
IPAddr.new("2001:20::/28"), # ORCHIDv2
IPAddr.new("2001:db8::/32"), # Addresses used in documentation and example source code
IPAddr.new("2002::/16"), # 6to4
IPAddr.new("fc00::/7"), # Unique local address
IPAddr.new("fe80::/10"), # Link-local address
IPAddr.new("ff00::/8"), # Multicast
] + IPV4_BLACKLIST.flat_map do |ipaddr|
prefixlen = ipaddr.prefixlen
ipv4_compatible = ipaddr.ipv4_compat.mask(96 + prefixlen)
ipv4_mapped = ipaddr.ipv4_mapped.mask(80 + prefixlen)
[ipv4_compatible, ipv4_mapped]
end).freeze
class << self
def extra_options(options)
options.merge(allowed_schemes: %w[https http])
end
def unsafe_ip_address?(ipaddr)
range = ipaddr.to_range
return true if range.first != range.last
return IPV6_BLACKLIST.any? { |r| r.include?(ipaddr) } if ipaddr.ipv6?
IPV4_BLACKLIST.any? { |r| r.include?(ipaddr) } # then it's IPv4
end
end
# adds support for the following options:
#
# :allowed_schemes :: list of URI schemes allowed (defaults to <tt>["https", "http"]</tt>)
module OptionsMethods
private
def option_allowed_schemes(value)
Array(value)
end
end
module InstanceMethods
def send_requests(*requests)
responses = requests.map do |request|
next if @options.allowed_schemes.include?(request.uri.scheme)
error = ServerSideRequestForgeryError.new("#{request.uri} URI scheme not allowed")
error.set_backtrace(caller)
response = ErrorResponse.new(request, error)
request.emit(:response, response)
response
end
allowed_requests = requests.select { |req| responses[requests.index(req)].nil? }
allowed_responses = super(*allowed_requests)
allowed_responses.each_with_index do |res, idx|
req = allowed_requests[idx]
responses[requests.index(req)] = res
end
responses
end
end
module ConnectionMethods
def initialize(*)
begin
super
rescue ServerSideRequestForgeryError => e
# may raise when IPs are passed as options via :addresses
throw(:resolve_error, e)
end
end
def addresses=(addrs)
addrs.reject!(&SsrfFilter.method(:unsafe_ip_address?))
raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?
super
end
end
end
register_plugin :ssrf_filter, SsrfFilter
end
end
|