surfguard 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +48 -23
- data/SECURITY.md +8 -4
- data/lib/surfguard/version.rb +1 -1
- data/lib/surfguard.rb +93 -18
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aacbc35ed38c3d141b4feec39de01c2f1ba3abc32c4005c1473e9572d7e19153
|
|
4
|
+
data.tar.gz: 208a710d8914dde70f9df917052ef2f488945889d66b91c14ec0f4fc9a839ac3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f363b4ae2a0f75f5de64e4c160b7080944183cbd062eba85838f91518cfd3526a59ca64378499d9c66f582bdf8aeacb9325486c760e3ea0f3f7e22fc39015a5f
|
|
7
|
+
data.tar.gz: 5af39621dd81510a02136bd56c7b90eb9e5c1578ab79910b05f80f4a9c869c4686288b6d553cb4b7ec1366f6ead25dba2f5f0d92db195d1cdebdb2c2d0ccbf37
|
data/README.md
CHANGED
|
@@ -7,19 +7,10 @@ One SSRF address policy for Ruby apps that fetch a URL someone else supplied.
|
|
|
7
7
|
It consolidates several drifting in-house copies of this policy into one — copies that had grown
|
|
8
8
|
four different ideas of what "internal" means, including one that decoded a NAT64 prefix whose length
|
|
9
9
|
is not recoverable from the address. This gem is their union, decided once and tested against the
|
|
10
|
-
|
|
10
|
+
IPv4 × IPv6 range matrix it enforces.
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
|
-
Not yet published to RubyGems — `v0.1.0` will be the first packaged release. Until it ships,
|
|
15
|
-
install from source:
|
|
16
|
-
|
|
17
|
-
```ruby
|
|
18
|
-
gem "surfguard", github: "basecamp/surfguard"
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Once `v0.1.0` is published:
|
|
22
|
-
|
|
23
14
|
```ruby
|
|
24
15
|
gem "surfguard"
|
|
25
16
|
```
|
|
@@ -35,7 +26,7 @@ Surfguard.resolve_public_ips("feeds.example.com")
|
|
|
35
26
|
# => ["93.184.216.34", "2606:2800:220:1:248:1893:25c8:1946"] (IPv4 first, blocked removed)
|
|
36
27
|
# Iterate THIS list on failover; do not resolve again inside a retry loop.
|
|
37
28
|
|
|
38
|
-
# Non-pinning
|
|
29
|
+
# Non-pinning preflight (hands the hostname to Net::HTTP, which resolves again):
|
|
39
30
|
Surfguard.resolvable_public_ip?("https://feeds.example.com/atom") # => true only if EVERY address is public
|
|
40
31
|
Surfguard.enforce_public_ip(url) # raises otherwise
|
|
41
32
|
|
|
@@ -46,6 +37,21 @@ Surfguard.resolve_public_ip(url) # => "93.184.216.34" or nil
|
|
|
46
37
|
Surfguard.blocked_address?(IPAddr.new("169.254.169.254")) # => true
|
|
47
38
|
```
|
|
48
39
|
|
|
40
|
+
The non-pinning helpers conservatively require every address in their lookup to be public. They do
|
|
41
|
+
not bind the later connection to that answer, so a second lookup can still change underneath them.
|
|
42
|
+
Pinning is required when attacker-controlled DNS or DNS rebinding is in scope.
|
|
43
|
+
|
|
44
|
+
The direct address APIs accept endpoint addresses, not networks. A prefix shorter than `/32` or
|
|
45
|
+
`/128` is refused rather than normalized into a host: `blocked_address?("10.0.0.1/8")` is `true`,
|
|
46
|
+
and `resolve_public_ips("10.0.0.1/8")` is `[]`.
|
|
47
|
+
|
|
48
|
+
### Caller responsibilities
|
|
49
|
+
|
|
50
|
+
Surfguard classifies an address; it is not an HTTP client. The URL helpers do not restrict schemes,
|
|
51
|
+
follow redirects, or make the request. Allow only the schemes your fetcher supports, and validate
|
|
52
|
+
and pin every redirect target just as you did the original target. A pinning client must connect to
|
|
53
|
+
the selected returned address while retaining the original hostname for HTTP Host and TLS identity.
|
|
54
|
+
|
|
49
55
|
## Refused vs. unresolvable
|
|
50
56
|
|
|
51
57
|
"We refuse that address" and "the host didn't answer" are different answers, and
|
|
@@ -71,35 +77,54 @@ question it was asked and never raises; use `enforce_public_ip` or
|
|
|
71
77
|
|---|---|
|
|
72
78
|
| IPv4 private (10/8, 172.16/12, 192.168/16), loopback (127/8), link-local (169.254/16) | refuse |
|
|
73
79
|
| CGNAT (100.64/10), benchmark (198.18/15), TEST-NETs, IETF (192.0.0/24), 6to4 relay anycast (192.88.99/24), multicast (224/4), reserved (240/4), "this" (0/8) | refuse |
|
|
74
|
-
| IPv6 ULA (fc00::/7, incl. IMDSv6 `fd00:ec2::254`), loopback (::1), link-local (fe80::/10), site-local (fec0::/10), multicast (ff00::/8), unspecified (::), discard (100::/64
|
|
80
|
+
| IPv6 ULA (fc00::/7, incl. IMDSv6 `fd00:ec2::254`), loopback (::1), link-local (fe80::/10), site-local (fec0::/10), multicast (ff00::/8), unspecified (::), discard/dummy (100::/64, 100:0:0:1::/64), documentation (2001:db8::/32, 3fff::/20), SRv6 SID (5f00::/16) | refuse |
|
|
81
|
+
| IETF protocol assignments (2001::/23) | refuse by default, including Teredo, benchmark, PCP/TURN/DNS-SD anycast to nearby infrastructure, deprecated ORCHID, ORCHIDv2, DET, and unallocated space; allow only AMT (2001:3::/32) and AS112-v6 (2001:4:112::/48) |
|
|
75
82
|
| IPv4-mapped `::ffff:0:0/96`, IPv4-compatible `::/96` | refuse outright |
|
|
76
83
|
| **SIIT `::ffff:0:0:0/96`** | decode embedded IPv4 (low 32 bits), re-check |
|
|
77
84
|
| NAT64 well-known `64:ff9b::/96` | decode embedded IPv4 (low 32 bits), re-check |
|
|
78
85
|
| **RFC 8215 NAT64 local-use `64:ff9b:1::/48`** | **refuse outright** — the Pref64 length is not recoverable from the address (RFC 6052 §2.2), so a low-32-bit decode reads the wrong octets; and the block is never globally routed |
|
|
79
86
|
| 6to4 `2002::/16` | refuse (a 6to4 address is just an IPv4 address in disguise) |
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
This is an SSRF address policy, not a label-only copy of the IANA special-purpose registry. It
|
|
89
|
+
refuses internal, non-destination, non-global domain, and overlay-identifier ranges plus transition
|
|
90
|
+
encodings that can conceal a blocked target. Actual globally reachable IP-layer services such as
|
|
91
|
+
AMT and AS112 remain valid; ORCHID and DET identifiers do not, because they can be interpreted by a
|
|
92
|
+
local overlay rather than routed as ordinary public IP destinations.
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
## Three things worth knowing
|
|
95
|
+
|
|
96
|
+
**1. Numeric parsing and name resolution are both part of the policy.** Before asking DNS,
|
|
97
|
+
Surfguard asks the system numeric-host parser used by `Socket`/`Net::HTTP` whether the token is an
|
|
98
|
+
address. This recognizes non-canonical decimal, hexadecimal, octal, and shortened IPv4 forms. A
|
|
99
|
+
numeric token is classified directly and is never sent through DNS or a search domain.
|
|
100
|
+
|
|
101
|
+
Names resolve with `Resolv.getaddresses`, which uses Ruby's usual hosts-plus-DNS chain, honours
|
|
102
|
+
search domains, and returns every address. The obvious alternatives each drop something a guard
|
|
103
|
+
can't afford to lose:
|
|
86
104
|
|
|
87
105
|
- `Resolv.getaddress` honours `/etc/hosts` but returns only the **first** address — so an AAAA-only
|
|
88
106
|
host deterministically takes the IPv6 path, and a multi-homed host is validated on one address
|
|
89
107
|
while the connection may use another.
|
|
90
|
-
- `Resolv::DNS.open` returns every address but **ignores** `/etc/hosts` and
|
|
91
|
-
|
|
92
|
-
connect to. Demonstrable: `Resolv.getaddresses("localhost")` → `["::1","127.0.0.1"]` while
|
|
93
|
-
`Resolv::DNS.open`'s `each_address("localhost")` → `["::","0.0.0.0"]`.
|
|
108
|
+
- `Resolv::DNS.open` returns every DNS address but **ignores** `/etc/hosts` and the other parts of
|
|
109
|
+
the default resolver chain.
|
|
94
110
|
|
|
95
|
-
`
|
|
96
|
-
|
|
111
|
+
Ruby `Resolv` is not a universal substitute for the system `getaddrinfo` name-service chain. A host
|
|
112
|
+
with custom NSS sources such as mDNS or LDAP can produce a different answer at connection time.
|
|
113
|
+
Pinning a returned address avoids that second name lookup; non-pinning deployments need a resolver
|
|
114
|
+
configuration in which Ruby's hosts-plus-DNS answers match the connection layer, or an independent
|
|
115
|
+
egress control. Resolver-chain equivalence does not eliminate the separate DNS-rebinding window.
|
|
97
116
|
|
|
98
117
|
**2. A resolver-level "no AAAA" switch is not a mitigation.** Disabling AAAA at the system resolver
|
|
99
118
|
(for example Kamal's `dns-opt: no-aaaa`) is a glibc `getaddrinfo` option. Surfguard resolves through
|
|
100
119
|
pure-Ruby `Resolv`, which requests AAAA regardless, so IPv6 answers still reach it. Don't treat that
|
|
101
120
|
deploy setting as if it narrowed Surfguard's input.
|
|
102
121
|
|
|
122
|
+
**3. Custom DNS64 prefixes need their own enforcement.** Surfguard can decode and re-check the
|
|
123
|
+
fixed NAT64 well-known prefix `64:ff9b::/96`, and it refuses the RFC 8215 local-use block outright.
|
|
124
|
+
It cannot infer the embedded IPv4 address in an arbitrary network-specific Pref64 from the
|
|
125
|
+
synthesized IPv6 address alone. A deployment using DNS64 with another Pref64 must enforce the same
|
|
126
|
+
blocked-address policy at its DNS64/NAT64 or egress layer.
|
|
127
|
+
|
|
103
128
|
## Testing
|
|
104
129
|
|
|
105
130
|
```bash
|
|
@@ -116,5 +141,5 @@ issue tracker.
|
|
|
116
141
|
## Status
|
|
117
142
|
|
|
118
143
|
Extracted and consolidated from several in-house SSRF guards, tested against the full
|
|
119
|
-
|
|
144
|
+
policy matrix. Resolve-and-classify only; callers pin. See the
|
|
120
145
|
[releases page](https://github.com/basecamp/surfguard/releases) for versions and changes.
|
data/SECURITY.md
CHANGED
|
@@ -23,16 +23,20 @@ Anything that lets a blocked address through, for example:
|
|
|
23
23
|
- An address in a blocked range that Surfguard classifies as public.
|
|
24
24
|
- A resolution path (encoding, embedding, transition mechanism) that reaches a blocked address
|
|
25
25
|
despite validation.
|
|
26
|
-
- A
|
|
27
|
-
|
|
26
|
+
- A parser or resolver discrepancy that lets a caller following a documented API contract connect
|
|
27
|
+
to a different address than Surfguard classified.
|
|
28
|
+
|
|
29
|
+
Surfguard does not perform the fetch itself. Scheme restrictions, redirect handling, connection
|
|
30
|
+
pinning, custom DNS64 prefixes, and nonstandard NSS sources have caller or deployment requirements
|
|
31
|
+
documented in the README. Those boundaries by themselves are not classification bugs, but a bypass
|
|
32
|
+
when the documented contract is followed qualifies.
|
|
28
33
|
|
|
29
34
|
Non-security bugs and questions belong in the
|
|
30
35
|
[regular issue tracker](https://github.com/basecamp/surfguard/issues).
|
|
31
36
|
|
|
32
37
|
## Supported versions
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
becomes the latest released version. Fixes ship as a new release, not backports.
|
|
39
|
+
The latest released version. Fixes ship as a new release, not backports.
|
|
36
40
|
|
|
37
41
|
## What to expect
|
|
38
42
|
|
data/lib/surfguard/version.rb
CHANGED
data/lib/surfguard.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "ipaddr"
|
|
4
4
|
require "resolv"
|
|
5
|
+
require "socket"
|
|
5
6
|
require "uri"
|
|
6
7
|
|
|
7
8
|
require_relative "surfguard/version"
|
|
@@ -15,13 +16,16 @@ require_relative "surfguard/version"
|
|
|
15
16
|
# Two policy decisions the copies drifted on, settled here and explained where
|
|
16
17
|
# they live in the code:
|
|
17
18
|
#
|
|
18
|
-
# * Resolver (see .resolve_public_ips):
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
19
|
+
# * Resolver (see .resolve_public_ips): first use getaddrinfo's numeric-only
|
|
20
|
+
# parser so legacy decimal, hex, octal, and short IPv4 forms mean exactly
|
|
21
|
+
# what they do to Net::HTTP; never send those tokens to DNS. Names resolve
|
|
22
|
+
# with Resolv.getaddresses, which honours /etc/hosts and search domains AND
|
|
23
|
+
# returns every address. One copy used Resolv.getaddress (honours hosts, but
|
|
24
|
+
# only the FIRST address, so an AAAA-only host deterministically selected the
|
|
25
|
+
# IPv6 path); another used Resolv::DNS.open (all addresses, but ignores
|
|
26
|
+
# /etc/hosts, so it validated a different set than Net::HTTP would connect
|
|
27
|
+
# to). getaddresses is complete for Ruby's hosts-plus-DNS chain; system
|
|
28
|
+
# getaddrinfo may additionally consult NSS sources such as mDNS or LDAP.
|
|
25
29
|
#
|
|
26
30
|
# * `no-aaaa` in Kamal `dns-opt` is NOT a mitigation. It is a glibc
|
|
27
31
|
# getaddrinfo option; every guard here resolves through pure-Ruby Resolv,
|
|
@@ -39,6 +43,12 @@ require_relative "surfguard/version"
|
|
|
39
43
|
module Surfguard
|
|
40
44
|
class Violation < StandardError; end
|
|
41
45
|
|
|
46
|
+
# Internal signal for something IPAddr can parse, but which is a network
|
|
47
|
+
# rather than a host. Keep it separate from a lookup failure: callers must
|
|
48
|
+
# refuse these inputs without asking DNS to reinterpret them as names.
|
|
49
|
+
class InvalidHost < ArgumentError; end
|
|
50
|
+
private_constant :InvalidHost
|
|
51
|
+
|
|
42
52
|
# The host answered with no address at all. Deliberately NOT a Violation:
|
|
43
53
|
# nothing was refused here, the lookup simply came back empty, which is
|
|
44
54
|
# usually a transient upstream failure. A caller that gives up permanently on
|
|
@@ -78,14 +88,31 @@ module Surfguard
|
|
|
78
88
|
DISALLOWED_IPV6 = [
|
|
79
89
|
IPAddr.new("::/128"), # Unspecified (RFC 4291)
|
|
80
90
|
IPAddr.new("100::/64"), # Discard-only (RFC 6666)
|
|
91
|
+
IPAddr.new("100:0:0:1::/64"), # Dummy IPv6 destination (RFC 9780)
|
|
81
92
|
IPAddr.new("2001::/32"), # Teredo (RFC 4380)
|
|
82
93
|
IPAddr.new("2001:2::/48"), # Benchmark testing (RFC 5180)
|
|
83
94
|
IPAddr.new("2001:db8::/32"), # Documentation (RFC 3849)
|
|
84
95
|
IPAddr.new("2002::/16"), # 6to4 (RFC 3056)
|
|
96
|
+
IPAddr.new("3fff::/20"), # Documentation (RFC 9637)
|
|
97
|
+
IPAddr.new("5f00::/16"), # SRv6 SIDs, confined to SR domains (RFC 9602)
|
|
85
98
|
IPAddr.new("fec0::/10"), # Deprecated site-local (RFC 3879)
|
|
86
99
|
IPAddr.new("ff00::/8") # Multicast (RFC 4291)
|
|
87
100
|
].freeze
|
|
88
101
|
|
|
102
|
+
# The IANA parent assignment is non-source, non-destination, non-forwardable,
|
|
103
|
+
# and non-global unless a more-specific allocation says otherwise. Refuse it
|
|
104
|
+
# by default so unallocated/future-special space cannot become an internal
|
|
105
|
+
# fetch path. These are the current, narrowly scoped IP-layer services whose
|
|
106
|
+
# registrations explicitly permit globally reachable destinations without
|
|
107
|
+
# selecting nearby infrastructure. PCP, TURN, and DNS-SD SRP anycast remain
|
|
108
|
+
# refused because they deliberately target on-path or local services. ORCHID,
|
|
109
|
+
# ORCHIDv2, and DET are overlay identifiers, not ordinary IP-layer locators.
|
|
110
|
+
IETF_PROTOCOL_ASSIGNMENTS = IPAddr.new("2001::/23")
|
|
111
|
+
GLOBALLY_REACHABLE_IETF_ASSIGNMENTS = [
|
|
112
|
+
IPAddr.new("2001:3::/32"), # AMT (RFC 7450)
|
|
113
|
+
IPAddr.new("2001:4:112::/48") # AS112-v6 (RFC 7535)
|
|
114
|
+
].freeze
|
|
115
|
+
|
|
89
116
|
# NAT64 embeds an IPv4 target that must be re-checked as IPv4. The well-known
|
|
90
117
|
# prefix is a fixed /96, so the embedded octets are always the low 32 bits:
|
|
91
118
|
# decode and re-check them, and NAT64 to a public address still resolves.
|
|
@@ -123,6 +150,8 @@ module Surfguard
|
|
|
123
150
|
.partition(&:ipv4?)
|
|
124
151
|
.flatten
|
|
125
152
|
.map(&:to_s)
|
|
153
|
+
rescue InvalidHost
|
|
154
|
+
[]
|
|
126
155
|
end
|
|
127
156
|
|
|
128
157
|
# True only if the URL's host resolves to at least one address and NONE are
|
|
@@ -171,6 +200,7 @@ module Surfguard
|
|
|
171
200
|
# it cannot parse is blocked.
|
|
172
201
|
def blocked_address?(ip)
|
|
173
202
|
ipaddr = ip.is_a?(IPAddr) ? ip : IPAddr.new(ip.to_s)
|
|
203
|
+
return true unless host_address?(ipaddr)
|
|
174
204
|
|
|
175
205
|
# DNS never legitimately returns an IPv4 address embedded these two ways, so
|
|
176
206
|
# refuse them regardless of the address they wrap.
|
|
@@ -191,15 +221,19 @@ module Surfguard
|
|
|
191
221
|
|
|
192
222
|
private
|
|
193
223
|
|
|
194
|
-
#
|
|
195
|
-
# internal literal is still caught by blocked_address?.
|
|
196
|
-
#
|
|
197
|
-
#
|
|
224
|
+
# A numeric host skips DNS so a public literal URL resolves directly and an
|
|
225
|
+
# internal literal is still caught by blocked_address?. Socket's numeric-only
|
|
226
|
+
# parser comes first because it accepts every legacy IPv4 spelling the
|
|
227
|
+
# connection layer does (decimal, hex, octal, and shortened forms), while
|
|
228
|
+
# IPAddr does not. Otherwise resolve via Resolv.getaddresses, returning every
|
|
229
|
+
# address from Ruby's hosts-plus-DNS chain. System getaddrinfo may consult
|
|
230
|
+
# additional NSS sources; pinning an address returned here avoids that second
|
|
231
|
+
# resolution. Returns [IPAddr].
|
|
198
232
|
def resolve(host)
|
|
199
|
-
|
|
233
|
+
literals = numeric_literals(host)
|
|
200
234
|
|
|
201
|
-
if
|
|
202
|
-
|
|
235
|
+
if literals
|
|
236
|
+
literals
|
|
203
237
|
else
|
|
204
238
|
Resolv.getaddresses(host).map { |a| IPAddr.new(a) }
|
|
205
239
|
end
|
|
@@ -207,15 +241,53 @@ module Surfguard
|
|
|
207
241
|
[]
|
|
208
242
|
end
|
|
209
243
|
|
|
210
|
-
#
|
|
211
|
-
# call
|
|
212
|
-
#
|
|
244
|
+
# Every address Socket's numeric-only parser finds. AI_NUMERICHOST guarantees
|
|
245
|
+
# this call never falls through to DNS. Its success is authoritative: these
|
|
246
|
+
# tokens must not be reinterpreted as names by Resolv, because Net::HTTP's
|
|
247
|
+
# getaddrinfo call will reinterpret them as the numeric addresses returned
|
|
248
|
+
# here. IPAddr remains as a fallback for syntax it alone accepts, notably a
|
|
249
|
+
# full-width /32 or /128 host prefix. nil means the token is a name.
|
|
250
|
+
def numeric_literals(host)
|
|
251
|
+
Socket.getaddrinfo(
|
|
252
|
+
host, nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM, 0, Socket::AI_NUMERICHOST
|
|
253
|
+
).map { |address| IPAddr.new(address[3]) }.uniq
|
|
254
|
+
rescue SocketError
|
|
255
|
+
literal = ip_literal(host)
|
|
256
|
+
return [ literal ] if literal
|
|
257
|
+
|
|
258
|
+
# An ordinary name may proceed to Resolv after AI_NUMERICHOST says it is
|
|
259
|
+
# not numeric. A token shaped like a legacy IPv4 spelling may not: if the
|
|
260
|
+
# system numeric parser itself failed abnormally, asking DNS would recreate
|
|
261
|
+
# the parser/resolver identity gap this method exists to close.
|
|
262
|
+
raise InvalidHost, "numeric-looking host #{host.inspect} could not be classified" if numeric_host_candidate?(host)
|
|
263
|
+
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def numeric_host_candidate?(host)
|
|
268
|
+
text = host.to_s
|
|
269
|
+
return true if text.include?(":") # Invalid IPv6 syntax is never a DNS name.
|
|
270
|
+
|
|
271
|
+
parts = text.split(".", -1)
|
|
272
|
+
(1..4).cover?(parts.length) &&
|
|
273
|
+
parts.all? { |part| part.match?(/\A(?:0[xX][0-9A-Fa-f]+|[0-9]+)\z/) }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# nil for anything IPAddr cannot parse. A shortened prefix is not a host and
|
|
277
|
+
# is rejected rather than normalized to its network address or sent to DNS.
|
|
213
278
|
def ip_literal(host)
|
|
214
|
-
IPAddr.new(host)
|
|
279
|
+
ipaddr = IPAddr.new(host)
|
|
280
|
+
raise InvalidHost, "#{host.inspect} is a network, not a host" unless host_address?(ipaddr)
|
|
281
|
+
|
|
282
|
+
ipaddr
|
|
215
283
|
rescue IPAddr::InvalidAddressError
|
|
216
284
|
nil
|
|
217
285
|
end
|
|
218
286
|
|
|
287
|
+
def host_address?(ipaddr)
|
|
288
|
+
ipaddr.prefix == (ipaddr.ipv4? ? 32 : 128)
|
|
289
|
+
end
|
|
290
|
+
|
|
219
291
|
def host_of(url)
|
|
220
292
|
# URI#host is "" for "http://" and nil when there's no authority at all.
|
|
221
293
|
# Neither is a name to look up, so both are malformed rather than a lookup
|
|
@@ -232,7 +304,10 @@ module Surfguard
|
|
|
232
304
|
end
|
|
233
305
|
|
|
234
306
|
def disallowed_ipv6?(ipaddr)
|
|
307
|
+
return false if GLOBALLY_REACHABLE_IETF_ASSIGNMENTS.any? { |range| range.include?(ipaddr) }
|
|
308
|
+
|
|
235
309
|
ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? ||
|
|
310
|
+
IETF_PROTOCOL_ASSIGNMENTS.include?(ipaddr) ||
|
|
236
311
|
DISALLOWED_IPV6.any? { |range| range.include?(ipaddr) }
|
|
237
312
|
end
|
|
238
313
|
|