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