mirror of
https://github.com/penpot/penpot.git
synced 2026-09-01 17:49:08 +00:00
🐛 Block IPv6 transition addresses in SSRF guard (#11320)
* 🐛 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 * ♻️ Remove dead embedded-IPv4 re-check from SSRF guard The previous commit added a recursive re-check of the IPv4 embedded in NAT64/6to4/Teredo addresses, but the `or` in `blocked-address?` short-circuits on the truthy keyword returned by `transition-prefix`, so the embedded-IPv4 branch was unreachable. The transition ranges are already rejected outright (fail-closed), making the re-check both unnecessary and untested. Remove `transition-embedded-ipv4`, simplify the IPv6 branch to a plain prefix check, and correct the docstrings and tests to match what the code actually does. AI-assisted-by: glm-5.3-flash
This commit is contained in:
parent
15195b3bbb
commit
326d83e780
@ -5,7 +5,12 @@
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(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."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
@ -122,6 +127,20 @@
|
||||
;; Check the embedded IPv4 is loopback (127.x.x.x)
|
||||
(= (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- blocked-address?
|
||||
"Check if an InetAddress should be blocked. Returns true if blocked."
|
||||
[^InetAddress addr]
|
||||
@ -141,12 +160,15 @@
|
||||
;; Cloud metadata IPs (exact match)
|
||||
(contains? cloud-metadata-ips (.getHostAddress addr))
|
||||
|
||||
;; Extra blocked CIDRs (IPv4 only)
|
||||
;; Extra blocked CIDRs (IPv4 only) and IPv6 transition mechanisms
|
||||
(let [bs (.getAddress addr)]
|
||||
(if (= (alength bs) 4)
|
||||
(or (some #(in-cidr4? bs %) extra-blocked-ranges)
|
||||
(some #(in-cidr4? bs %) extra-blocked-cidrs))
|
||||
false))))
|
||||
;; IPv6 transition mechanisms (NAT64/6to4/Teredo): the range is
|
||||
;; rejected outright.
|
||||
(boolean (when (= (alength bs) 16)
|
||||
(transition-prefix bs)))))))
|
||||
|
||||
(defn resolve-host
|
||||
"Resolve a hostname to all InetAddress objects. Wraps InetAddress/getAllByName
|
||||
@ -163,8 +185,10 @@
|
||||
- host must resolve to at least one address, and
|
||||
- **every** resolved address must NOT be in the blocklist
|
||||
(loopback, link-local, site-local, multicast, any-local,
|
||||
cloud-metadata 169.254.169.254, IPv6 ULA fc00::/7, IPv4-mapped
|
||||
IPv6 of any blocked IPv4, plus operator-supplied CIDRs).
|
||||
cloud-metadata 169.254.169.254, IPv6 ULA fc00::/7, IPv6 transition
|
||||
mechanisms NAT64 64:ff9b::/96, 6to4 2002::/16 and Teredo
|
||||
2001:0000::/32, IPv4-mapped IPv6 of any blocked IPv4,
|
||||
plus operator-supplied CIDRs).
|
||||
When the host is an IP literal (decimal/octal/hex/IPv6) it is
|
||||
normalized via `com.google.common.net.InetAddresses` before the
|
||||
check.
|
||||
|
||||
@ -80,6 +80,24 @@
|
||||
(t/is (false? (ssrf/safe-url? "http://[fd00::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
|
||||
(t/is (false? (ssrf/safe-url?
|
||||
"http://[2001:0000:4136:e378:8000:63bf:3fff:fdd2]/foo"))))
|
||||
|
||||
(t/deftest validate-url-blocks-encoded-loopback
|
||||
;; Decimal encoding of 127.0.0.1 = 2130706433
|
||||
;; InetAddress normalizes this to 127.0.0.1
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user