mirror of
https://github.com/penpot/penpot.git
synced 2026-08-29 16:18:48 +00:00
🐛 Block IPv6 transition addresses in SSRF guard
The outbound HTTP SSRF blocklist did not classify NAT64 (64:ff9b::/96), 6to4 (2002::/16) or Teredo (2001:0000::/32) addresses, whose embedded IPv4 target is invisible to the JVM InetAddress predicates, so URLs resolving to them could reach cloud metadata, loopback or RFC 1918 hosts from webhook delivery and media import. Transition ranges are now rejected outright and any embedded IPv4 is re-checked against the full blocklist, including operator-supplied extra blocked CIDRs. Closes #11319
This commit is contained in:
parent
9fa07e7468
commit
d39b135feb
@ -5,7 +5,12 @@
|
|||||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||||
|
|
||||||
(ns app.util.ssrf
|
(ns app.util.ssrf
|
||||||
"URL/host validation to prevent Server-Side Request Forgery."
|
"URL/host validation to prevent Server-Side Request Forgery.
|
||||||
|
|
||||||
|
The blocklist covers the standard JVM InetAddress classifications plus
|
||||||
|
explicit ranges: IPv6 ULA, IPv4-mapped loopback, cloud metadata,
|
||||||
|
operator-supplied CIDRs and the IPv6 transition mechanisms NAT64, 6to4
|
||||||
|
and Teredo (whose embedded IPv4 is also re-checked)."
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
@ -122,6 +127,35 @@
|
|||||||
;; Check the embedded IPv4 is loopback (127.x.x.x)
|
;; Check the embedded IPv4 is loopback (127.x.x.x)
|
||||||
(= (bit-and (aget bs 12) 0xFF) 127))))
|
(= (bit-and (aget bs 12) 0xFF) 127))))
|
||||||
|
|
||||||
|
(defn- transition-prefix
|
||||||
|
"Classify a 16-byte IPv6 address into its transition mechanism:
|
||||||
|
:nat64 (64:ff9b::/96), :6to4 (2002::/16), :teredo (2001:0000::/32) or nil."
|
||||||
|
[^bytes bs]
|
||||||
|
(let [b0 (bit-and (aget bs 0) 0xFF)
|
||||||
|
b1 (bit-and (aget bs 1) 0xFF)
|
||||||
|
b2 (bit-and (aget bs 2) 0xFF)
|
||||||
|
b3 (bit-and (aget bs 3) 0xFF)]
|
||||||
|
(cond
|
||||||
|
(and (= b0 0x00) (= b1 0x64) (= b2 0xFF) (= b3 0x9B)) :nat64
|
||||||
|
(and (= b0 0x20) (= b1 0x02)) :6to4
|
||||||
|
(and (= b0 0x20) (= b1 0x01) (= b2 0x00) (= b3 0x00)) :teredo
|
||||||
|
:else nil)))
|
||||||
|
|
||||||
|
(defn- transition-embedded-ipv4
|
||||||
|
"Extract the IPv4 address embedded in an IPv6 transition mechanism address,
|
||||||
|
as a 4-byte array. Returns nil when the address is not a transition address.
|
||||||
|
Teredo embeds the client IPv4 XOR-inverted in the last 4 bytes."
|
||||||
|
^bytes [^bytes bs]
|
||||||
|
(when (= (alength bs) 16)
|
||||||
|
(case (transition-prefix bs)
|
||||||
|
:nat64 (byte-array [(aget bs 12) (aget bs 13) (aget bs 14) (aget bs 15)])
|
||||||
|
:6to4 (byte-array [(aget bs 2) (aget bs 3) (aget bs 4) (aget bs 5)])
|
||||||
|
:teredo (byte-array [(bit-xor (bit-and (aget bs 12) 0xFF) 0xFF)
|
||||||
|
(bit-xor (bit-and (aget bs 13) 0xFF) 0xFF)
|
||||||
|
(bit-xor (bit-and (aget bs 14) 0xFF) 0xFF)
|
||||||
|
(bit-xor (bit-and (aget bs 15) 0xFF) 0xFF)])
|
||||||
|
nil)))
|
||||||
|
|
||||||
(defn- blocked-address?
|
(defn- blocked-address?
|
||||||
"Check if an InetAddress should be blocked. Returns true if blocked."
|
"Check if an InetAddress should be blocked. Returns true if blocked."
|
||||||
[^InetAddress addr]
|
[^InetAddress addr]
|
||||||
@ -146,7 +180,13 @@
|
|||||||
(if (= (alength bs) 4)
|
(if (= (alength bs) 4)
|
||||||
(or (some #(in-cidr4? bs %) extra-blocked-ranges)
|
(or (some #(in-cidr4? bs %) extra-blocked-ranges)
|
||||||
(some #(in-cidr4? bs %) extra-blocked-cidrs))
|
(some #(in-cidr4? bs %) extra-blocked-cidrs))
|
||||||
false))))
|
;; IPv6 transition mechanisms (NAT64/6to4/Teredo): the range is blocked
|
||||||
|
;; outright and any embedded IPv4 is re-checked against this blocklist.
|
||||||
|
(boolean
|
||||||
|
(when (= (alength bs) 16)
|
||||||
|
(or (transition-prefix bs)
|
||||||
|
(when-let [embedded (transition-embedded-ipv4 bs)]
|
||||||
|
(blocked-address? (InetAddress/getByAddress embedded))))))))))
|
||||||
|
|
||||||
(defn resolve-host
|
(defn resolve-host
|
||||||
"Resolve a hostname to all InetAddress objects. Wraps InetAddress/getAllByName
|
"Resolve a hostname to all InetAddress objects. Wraps InetAddress/getAllByName
|
||||||
@ -163,8 +203,10 @@
|
|||||||
- host must resolve to at least one address, and
|
- host must resolve to at least one address, and
|
||||||
- **every** resolved address must NOT be in the blocklist
|
- **every** resolved address must NOT be in the blocklist
|
||||||
(loopback, link-local, site-local, multicast, any-local,
|
(loopback, link-local, site-local, multicast, any-local,
|
||||||
cloud-metadata 169.254.169.254, IPv6 ULA fc00::/7, IPv4-mapped
|
cloud-metadata 169.254.169.254, IPv6 ULA fc00::/7, IPv6 transition
|
||||||
IPv6 of any blocked IPv4, plus operator-supplied CIDRs).
|
mechanisms NAT64 64:ff9b::/96, 6to4 2002::/16 and Teredo
|
||||||
|
2001:0000::/32 — including any IPv4 embedded in them —,
|
||||||
|
IPv4-mapped IPv6 of any blocked IPv4, plus operator-supplied CIDRs).
|
||||||
When the host is an IP literal (decimal/octal/hex/IPv6) it is
|
When the host is an IP literal (decimal/octal/hex/IPv6) it is
|
||||||
normalized via `com.google.common.net.InetAddresses` before the
|
normalized via `com.google.common.net.InetAddresses` before the
|
||||||
check.
|
check.
|
||||||
|
|||||||
@ -80,6 +80,29 @@
|
|||||||
(t/is (false? (ssrf/safe-url? "http://[fd00::1]/foo")))
|
(t/is (false? (ssrf/safe-url? "http://[fd00::1]/foo")))
|
||||||
(t/is (false? (ssrf/safe-url? "http://[fc00::1]/foo"))))
|
(t/is (false? (ssrf/safe-url? "http://[fc00::1]/foo"))))
|
||||||
|
|
||||||
|
(t/deftest validate-url-blocks-nat64-encoded-metadata
|
||||||
|
;; 64:ff9b::a9fe:a9fe embeds 169.254.169.254 (cloud metadata)
|
||||||
|
(t/is (false? (ssrf/safe-url? "http://[64:ff9b::a9fe:a9fe]/latest/meta-data/"))))
|
||||||
|
|
||||||
|
(t/deftest validate-url-blocks-nat64-encoded-loopback
|
||||||
|
;; 64:ff9b::7f00:0001 embeds 127.0.0.1
|
||||||
|
(t/is (false? (ssrf/safe-url? "http://[64:ff9b::7f00:1]/foo"))))
|
||||||
|
|
||||||
|
(t/deftest validate-url-blocks-6to4-encoded-private
|
||||||
|
;; 2002:a00:1:: embeds 10.0.0.1; 2002:c0a8:101:: embeds 192.168.1.1
|
||||||
|
(t/is (false? (ssrf/safe-url? "http://[2002:a00:1::1]/foo")))
|
||||||
|
(t/is (false? (ssrf/safe-url? "http://[2002:c0a8:101::1]/foo"))))
|
||||||
|
|
||||||
|
(t/deftest validate-url-blocks-teredo-encoded-addresses
|
||||||
|
;; Teredo server prefix 2001:0000::/32, embedded client IPv4 is XOR-inverted
|
||||||
|
(t/is (false? (ssrf/safe-url?
|
||||||
|
"http://[2001:0000:4136:e378:8000:63bf:3fff:fdd2]/foo"))))
|
||||||
|
|
||||||
|
(t/deftest validate-url-extra-cidrs-apply-to-transition-addresses
|
||||||
|
;; 2002:c633:6407:: embeds 198.51.100.7, covered by the operator CIDR below
|
||||||
|
(binding [ssrf/extra-blocked-cidrs #{(ssrf/parse-cidr "198.51.100.0/24")}]
|
||||||
|
(t/is (false? (ssrf/safe-url? "http://[2002:c633:6407::1]/foo")))))
|
||||||
|
|
||||||
(t/deftest validate-url-blocks-encoded-loopback
|
(t/deftest validate-url-blocks-encoded-loopback
|
||||||
;; Decimal encoding of 127.0.0.1 = 2130706433
|
;; Decimal encoding of 127.0.0.1 = 2130706433
|
||||||
;; InetAddress normalizes this to 127.0.0.1
|
;; InetAddress normalizes this to 127.0.0.1
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user