From: W.C.A. Wijngaards Date: Thu, 5 Mar 2026 08:47:13 +0000 (+0100) Subject: - Fix for DNS Rebinding Bypass via SVCB/HTTPS Records in Unbound. X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=8f96ae7acf8275d40b49e06ac41242dc02c191ac;p=thirdparty%2Funbound.git - 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. --- diff --git a/doc/Changelog b/doc/Changelog index 19137495b..0230be806 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -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. diff --git a/doc/unbound.conf.rst b/doc/unbound.conf.rst index 0d406f689..fd4a7969c 100644 --- a/doc/unbound.conf.rst +++ b/doc/unbound.conf.rst @@ -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` that you configured is allowed to, and you can specify additional names using diff --git a/iterator/iter_priv.c b/iterator/iter_priv.c index be4219216..6f885cec7 100644 --- a/iterator/iter_priv.c +++ b/iterator/iter_priv.c @@ -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; } diff --git a/iterator/iter_scrub.c b/iterator/iter_scrub.c index 8507a3fb6..a4b98375b 100644 --- a/iterator/iter_scrub.c +++ b/iterator/iter_scrub.c @@ -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 index 000000000..5deae4250 --- /dev/null +++ b/testdata/iter_priv_svcb.rpl @@ -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