]> git.ipfire.org Git - thirdparty/unbound.git/commitdiff
- Fix for DNS Rebinding Bypass via SVCB/HTTPS Records in Unbound.
authorW.C.A. Wijngaards <wouter@nlnetlabs.nl>
Thu, 5 Mar 2026 08:47:13 +0000 (09:47 +0100)
committerW.C.A. Wijngaards <wouter@nlnetlabs.nl>
Thu, 5 Mar 2026 08:47:13 +0000 (09:47 +0100)
  Thanks to Kunta Chu, School of Software, Tsinghua University,
  Taofei Guo, Peking University, and Jianjun Chen, Institute for
  Network Sciences and Cyberspace, Tsinghua University for the
  report. The private-address option is fixed to also elide
  SVCB and HTTPS records that match the filter.

doc/Changelog
doc/unbound.conf.rst
iterator/iter_priv.c
iterator/iter_scrub.c
testdata/iter_priv_svcb.rpl [new file with mode: 0644]

index 19137495b270e830e77d4692ba7bc35ede6fdb19..0230be80611aeb62075759465e59c617f50fe559 100644 (file)
@@ -1,3 +1,11 @@
+5 March 2026: Wouter
+       - Fix for DNS Rebinding Bypass via SVCB/HTTPS Records in Unbound.
+         Thanks to Kunta Chu, School of Software, Tsinghua University,
+         Taofei Guo, Peking University, and Jianjun Chen, Institute for
+         Network Sciences and Cyberspace, Tsinghua University for the
+         report. The private-address option is fixed to also elide
+         SVCB and HTTPS records that match the filter.
+
 4 March 2026: Yorgos
        - For #1411: Introduce a failing case in the rpl test so that it only
          passes with the fix in place.
index 0d406f6890c666857286dc43771f971a3732e8e8..fd4a7969cde6bde0d7415493b9c061034c52ab9b 100644 (file)
@@ -2015,6 +2015,11 @@ These options are part of the ``server:`` section.
     turned into a network proxy, allowing remote access through the browser to
     other parts of your private network.
 
+    The option removes resource records of types A, AAAA, SVCB and HTTPS
+    that match the filter.
+    Inside the SVCB and HTTPS records, the svcparams of type ipv4hint
+    and ipv6hint are checked for matches.
+
     Some names can be allowed to contain your private addresses, by default all
     the :ref:`local-data<unbound.conf.local-data>` that you configured is
     allowed to, and you can specify additional names using
index be4219216a4cdd7f10d99f5117e595de7027d313..6f885cec7f85cdac4f943fb33c63b21ccc8569f0 100644 (file)
@@ -207,6 +207,168 @@ size_t priv_get_mem(struct iter_priv* priv)
        return sizeof(*priv) + regional_get_mem(priv->region);
 }
 
+/**
+ * Check if svcparam ipv4hint contains a private address.
+ * @param priv: private address lookup struct.
+ * @param d: the data bytes.
+ * @param data_len: number of data bytes in the svcparam.
+ * @param addr: address to return the private address to log in to.
+ *     It has space for IPv4 and IPv6 addresses.
+ * @param addrlen: length of the addr. Returns the correct size for the addr.
+ * @return true if the rdata contains a private address.
+ */
+static int svcb_ipv4hint_contains_priv_addr(struct iter_priv* priv,
+       uint8_t* d, uint16_t data_len, struct sockaddr_storage* addr,
+       socklen_t* addrlen)
+{
+       struct sockaddr_in sa;
+       *addrlen = (socklen_t)sizeof(struct sockaddr_in);
+       memset(&sa, 0, sizeof(struct sockaddr_in));
+       sa.sin_family = AF_INET;
+       sa.sin_port = (in_port_t)htons(UNBOUND_DNS_PORT);
+
+       while(data_len >= LDNS_IP4ADDRLEN) {
+               memmove(&sa.sin_addr, d, LDNS_IP4ADDRLEN);
+               memmove(addr, &sa, *addrlen);
+               if(priv_lookup_addr(priv, addr, *addrlen))
+                       return 1;
+
+               d += LDNS_IP4ADDRLEN;
+               data_len -= LDNS_IP4ADDRLEN;
+       }
+       /* if data_len != 0 here, then the svcparam is malformed. */
+       return 0;
+}
+
+/**
+ * Check if svcparam ipv6hint contains a private address.
+ * @param priv: private address lookup struct.
+ * @param d: the data bytes.
+ * @param data_len: number of data bytes in the svcparam.
+ * @param addr: address to return the private address to log in to.
+ *     It has space for IPv4 and IPv6 addresses.
+ * @param addrlen: length of the addr. Returns the correct size for the addr.
+ * @return true if the rdata contains a private address.
+ */
+static int svcb_ipv6hint_contains_priv_addr(struct iter_priv* priv,
+       uint8_t* d, uint16_t data_len, struct sockaddr_storage* addr,
+       socklen_t* addrlen)
+{
+       struct sockaddr_in6 sa;
+       *addrlen = (socklen_t)sizeof(struct sockaddr_in6);
+       memset(&sa, 0, sizeof(struct sockaddr_in6));
+       sa.sin6_family = AF_INET6;
+       sa.sin6_port = (in_port_t)htons(UNBOUND_DNS_PORT);
+
+       while(data_len >= LDNS_IP6ADDRLEN) {
+               memmove(&sa.sin6_addr, d, LDNS_IP6ADDRLEN);
+               memmove(addr, &sa, *addrlen);
+               if(priv_lookup_addr(priv, addr, *addrlen))
+                       return 1;
+
+               d += LDNS_IP6ADDRLEN;
+               data_len -= LDNS_IP6ADDRLEN;
+       }
+       /* if data_len != 0 here, then the svcparam is malformed. */
+       return 0;
+}
+
+/**
+ * Check if type SVCB and HTTPS rdata contains a private address.
+ * @param priv: private address lookup struct.
+ * @param pkt: the packet.
+ * @param rr: the rr with rdata to check.
+ * @param addr: address to return the private address to log in to.
+ * @param addrlen: length of the addr. Initially the total size, on
+ *     return the correct size for the addr.
+ * @return true if the rdata contains a private address.
+ */
+static int svcb_rr_contains_priv_addr(struct iter_priv* priv,
+       sldns_buffer* pkt, struct rr_parse* rr, struct sockaddr_storage* addr,
+       socklen_t* addrlen)
+{
+       uint8_t* d = rr->ttl_data;
+       uint16_t svcparamkey, data_len, rdatalen;
+       size_t oldpos, dname_len, dname_start, dname_compr_len;
+       d += 4; /* skip TTL */
+       rdatalen = sldns_read_uint16(d); /* read rdata length */
+       d += 2;
+
+       if(rdatalen < 2 /* priority */ + 1 /* 1 length target */)
+               return 0; /* malformed, too short */
+       d += 2; /* skip priority */
+       rdatalen -= 2;
+       oldpos = sldns_buffer_position(pkt);
+       sldns_buffer_set_position(pkt, (size_t)(d - sldns_buffer_begin(pkt)));
+       dname_start = sldns_buffer_position(pkt);
+       dname_len = pkt_dname_len(pkt);
+       dname_compr_len = sldns_buffer_position(pkt) - dname_start;
+       sldns_buffer_set_position(pkt, oldpos);
+       if(dname_len == 0)
+               return 0; /* dname malformed */
+       if(dname_compr_len > rdatalen)
+               return 0; /* malformed */
+       d += dname_compr_len; /* skip target */
+       rdatalen -= dname_compr_len;
+
+       while(rdatalen >= 4) {
+               svcparamkey = sldns_read_uint16(d);
+               data_len = sldns_read_uint16(d+2);
+               d += 4;
+               rdatalen -= 4;
+
+               /* verify that we have data_len data */
+               if(data_len > rdatalen) {
+                       /* It is malformed, but if there are addresses
+                        * in there it can be rejected. */
+                       data_len = rdatalen;
+               }
+
+               if(!data_len)
+                       continue; /* no data for the svcparamkey */
+
+               if(svcparamkey == SVCB_KEY_IPV4HINT) {
+                       if(svcb_ipv4hint_contains_priv_addr(priv, d, data_len,
+                               addr, addrlen))
+                               return 1;
+               } else if(svcparamkey == SVCB_KEY_IPV6HINT) {
+                       if(svcb_ipv6hint_contains_priv_addr(priv, d, data_len,
+                               addr, addrlen))
+                               return 1;
+               }
+               d += data_len;
+               rdatalen -= data_len;
+       }
+       /* If rdatalen != 0 here, then the svcb rdata is malformed. */
+       return 0;
+}
+
+/**
+ * Check if the SVCB and HTTPS rrset is bad.
+ * @param priv: private address lookup struct.
+ * @param pkt: the packet.
+ * @param rrset: the rrset to check.
+ * @return 1 if the entire rrset has to be removed. 0 if not.
+ * It removes RRs if they have private addresses, and log that.
+ */
+static int priv_svcb_rrset_bad(struct iter_priv* priv, sldns_buffer* pkt,
+       struct rrset_parse* rrset)
+{
+       struct rr_parse* rr, *prev = NULL;
+       struct sockaddr_storage addr;
+       socklen_t addrlen = (socklen_t)sizeof(addr);
+       for(rr = rrset->rr_first; rr; rr = rr->next) {
+               if(svcb_rr_contains_priv_addr(priv, pkt, rr, &addr,
+                       &addrlen)) {
+                       if(msgparse_rrset_remove_rr("sanitize: removing public name with private address", pkt, rrset, prev, rr, &addr, addrlen))
+                               return 1;
+                       continue;
+               }
+               prev = rr;
+       }
+       return 0;
+}
+
 int priv_rrset_bad(struct iter_priv* priv, sldns_buffer* pkt,
        struct rrset_parse* rrset)
 {
@@ -268,7 +430,11 @@ int priv_rrset_bad(struct iter_priv* priv, sldns_buffer* pkt,
                                }
                                prev = rr;
                        }
-               } 
+               } else if(rrset->type == LDNS_RR_TYPE_SVCB ||
+                       rrset->type == LDNS_RR_TYPE_HTTPS) {
+                       if(priv_svcb_rrset_bad(priv, pkt, rrset))
+                               return 1;
+               }
        }
        return 0;
 }
index 8507a3fb65acb7dad2c795fc1b46348f6b652520..a4b98375b0c06a7cd03b85a8bef554978c70746c 100644 (file)
@@ -972,8 +972,10 @@ scrub_sanitize(sldns_buffer* pkt, struct msg_parse* msg,
                }
 
                /* remove private addresses */
-               if( (rrset->type == LDNS_RR_TYPE_A || 
-                       rrset->type == LDNS_RR_TYPE_AAAA)) {
+               if(rrset->type == LDNS_RR_TYPE_A ||
+                       rrset->type == LDNS_RR_TYPE_AAAA ||
+                       rrset->type == LDNS_RR_TYPE_SVCB ||
+                       rrset->type == LDNS_RR_TYPE_HTTPS) {
 
                        /* do not set servfail since this leads to too
                         * many drops of other people using rfc1918 space */
diff --git a/testdata/iter_priv_svcb.rpl b/testdata/iter_priv_svcb.rpl
new file mode 100644 (file)
index 0000000..5deae42
--- /dev/null
@@ -0,0 +1,283 @@
+; config options
+server:
+       target-fetch-policy: "0 0 0 0 0"
+       qname-minimisation: no
+       minimal-responses: yes
+       iter-scrub-promiscuous: yes
+
+       private-address: 10.0.0.0/8
+       private-address: 172.16.0.0/12
+       private-address: 192.168.0.0/16
+       private-address: 169.254.0.0/16
+       private-address: fd00::/8
+       private-address: fe80::/10
+
+       private-domain: "example.net"
+
+stub-zone:
+       name: "."
+       stub-addr: 193.0.14.129         # K.ROOT-SERVERS.NET.
+
+CONFIG_END
+
+SCENARIO_BEGIN Test iterator scrubber with private addresses in SVCB.
+
+; K.ROOT-SERVERS.NET.
+RANGE_BEGIN 0 100
+       ADDRESS 193.0.14.129 
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+. IN NS        K.ROOT-SERVERS.NET.
+SECTION ADDITIONAL
+K.ROOT-SERVERS.NET.    IN      A       193.0.14.129
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+com. IN A
+SECTION AUTHORITY
+com.   IN NS   a.gtld-servers.net.
+SECTION ADDITIONAL
+a.gtld-servers.net.    IN      A       192.5.6.30
+ENTRY_END
+
+; root server authoritative for example.net too.
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+mail.example.net. IN SVCB
+SECTION ANSWER
+mail.example.net. IN SVCB 1 foo.example.net. ipv4hint=10.20.30.40
+ENTRY_END
+RANGE_END
+
+; a.gtld-servers.net.
+RANGE_BEGIN 0 100
+       ADDRESS 192.5.6.30
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+com. IN NS
+SECTION ANSWER
+com.   IN NS   a.gtld-servers.net.
+SECTION ADDITIONAL
+a.gtld-servers.net.    IN      A       192.5.6.30
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+example.com. IN A
+SECTION AUTHORITY
+example.com.   IN NS   ns.example.com.
+SECTION ADDITIONAL
+ns.example.com.                IN      A       1.2.3.4
+ENTRY_END
+RANGE_END
+
+; ns.example.com.
+RANGE_BEGIN 0 100
+       ADDRESS 1.2.3.4
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+example.com. IN NS
+SECTION ANSWER
+example.com.   IN NS   ns.example.com.
+SECTION ADDITIONAL
+ns.example.com.                IN      A       1.2.3.4
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+ns.example.com. IN A
+SECTION ANSWER
+ns.example.com.                IN      A       1.2.3.4
+SECTION AUTHORITY
+example.com.   IN NS   ns.example.com.
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+ns.example.com. IN AAAA
+SECTION ANSWER
+SECTION AUTHORITY
+example.com. IN SOA ns.example.com. root.example.com. 4 14400 3600 604800 3600
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+www.example.com. IN SVCB
+SECTION ANSWER
+www.example.com. IN SVCB 1 foo.example.com. ipv4hint=192.20.30.40
+SECTION AUTHORITY
+example.com.   IN NS   ns.example.com.
+SECTION ADDITIONAL
+ns.example.com.                IN      A       1.2.3.4
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+mail.example.com. IN SVCB
+SECTION ANSWER
+mail.example.com. IN SVCB 1 foo.example.com. ipv6hint=fe80::15
+SECTION AUTHORITY
+example.com.   IN NS   ns.example.com.
+SECTION ADDITIONAL
+ns.example.com.                IN      A       1.2.3.4
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+foo.example.com. IN SVCB
+SECTION ANSWER
+foo.example.com. IN SVCB 1 foo.example.com. ipv4hint=10.20.30.40
+SECTION AUTHORITY
+example.com.   IN NS   ns.example.com.
+SECTION ADDITIONAL
+ns.example.com.                IN      A       1.2.3.4
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+toss.example.com. IN SVCB
+SECTION ANSWER
+toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=10.20.30.40
+toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=10.20.30.40
+toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=1.2.3.4
+toss.example.com. IN SVCB 1 foo.example.com. ipv6hint=fe80::15
+toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=10.20.30.41
+toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=192.0.2.1,10.20.30.42,192.0.2.2
+SECTION AUTHORITY
+example.com.   IN NS   ns.example.com.
+SECTION ADDITIONAL
+ns.example.com.                IN      A       1.2.3.4
+ENTRY_END
+RANGE_END
+
+; public address is not scrubbed
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.example.com. IN SVCB
+ENTRY_END
+
+; recursion happens here.
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+www.example.com. IN SVCB
+SECTION ANSWER
+www.example.com. IN SVCB 1 foo.example.com. ipv4hint=192.20.30.40
+ENTRY_END
+
+; IPv4 address is scrubbed
+STEP 3 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+foo.example.com. IN SVCB
+ENTRY_END
+
+; recursion happens here.
+STEP 10 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+foo.example.com. IN SVCB
+SECTION ANSWER
+; scrubbed away
+ENTRY_END
+
+; IPv6 address is scrubbed
+STEP 20 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+mail.example.com. IN SVCB
+ENTRY_END
+
+STEP 30 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+mail.example.com. IN SVCB
+SECTION ANSWER
+ENTRY_END
+
+; allowed domain is not scrubbed.
+STEP 40 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+mail.example.net. IN SVCB
+ENTRY_END
+
+STEP 50 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+mail.example.net. IN SVCB
+SECTION ANSWER
+mail.example.net. IN SVCB 1 foo.example.net. ipv4hint=10.20.30.40
+ENTRY_END
+
+; rest of RRset intact, only 10/8 tossed away.
+STEP 60 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+toss.example.com. IN SVCB
+ENTRY_END
+
+STEP 70 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+toss.example.com. IN SVCB
+SECTION ANSWER
+toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=1.2.3.4
+ENTRY_END
+
+SCENARIO_END