From: W.C.A. Wijngaards Date: Thu, 23 Jul 2026 14:17:59 +0000 (+0200) Subject: - Fix that the aggressive negative cache does not insert NSEC X-Git-Url: http://git.ipfire.org/index.cgi?a=commitdiff_plain;h=5eb362a6c0da075fbf810c7247fc2cdbe50bc6e0;p=thirdparty%2Funbound.git - Fix that the aggressive negative cache does not insert NSEC records with overreaching next owner name. Also the result is not above the trust anchor's bailiwick. Also RRSIGS are not considered valid when an NSEC next owner name is not under the signer zone name. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- diff --git a/doc/Changelog b/doc/Changelog index 112fc2465..fee4ed411 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -6,6 +6,12 @@ checked to be the same as the signer name. Also RRSIGs are not considered valid when an NSEC3 is not b32.signerzone. Thanks to Qifan Zhang, Palo Alto Networks, for the report. + - Fix that the aggressive negative cache does not insert NSEC + records with overreaching next owner name. Also the result + is not above the trust anchor's bailiwick. Also RRSIGS are + not considered valid when an NSEC next owner name is not + under the signer zone name. Thanks to Qifan Zhang, Palo + Alto Networks, for the report. 22 July 2026: Wouter - Release tag for 1.25.2, with the security commits: diff --git a/iterator/iterator.c b/iterator/iterator.c index 25d40f269..1f95039c8 100644 --- a/iterator/iterator.c +++ b/iterator/iterator.c @@ -1531,7 +1531,7 @@ processInitRequest(struct module_qstate* qstate, struct iter_qstate* iq, msg = val_neg_getmsg(qstate->env->neg_cache, &iq->qchase, qstate->region, qstate->env->rrset_cache, qstate->env->scratch_buffer, - *qstate->env->now, 1/*add SOA*/, NULL, + *qstate->env->now, 1/*add SOA*/, dpname, qstate->env->cfg); } /* item taken from cache does not match our query name, thus diff --git a/testdata/nsec_cross_zone.rpl b/testdata/nsec_cross_zone.rpl new file mode 100644 index 000000000..8973fae9f --- /dev/null +++ b/testdata/nsec_cross_zone.rpl @@ -0,0 +1,216 @@ +; config options +; The island of trust is at test. +server: + trust-anchor: "test. DS 1444 8 2 8a87d067fd09a5965244fe2e317dd26d182c468e0a7f26ecc4c7b479bf89db9b" + val-override-date: "20201020135527" + target-fetch-policy: "0 0 0 0 0" + qname-minimisation: "no" + fake-sha1: yes + trust-anchor-signaling: no + minimal-responses: no + iter-scrub-promiscuous: no + aggressive-nsec: yes + local-zone: test. nodefault + log-servfail: yes + +stub-zone: + name: "." + stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET. +CONFIG_END + +SCENARIO_BEGIN Test overreaching NSEC with aggressive cache + +; 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 +test. IN NS +SECTION AUTHORITY +test. IN NS ns.test. +SECTION ADDITIONAL +ns.test. IN A 1.2.3.5 +ENTRY_END +RANGE_END + +; ns.test +RANGE_BEGIN 0 100 + ADDRESS 1.2.3.5 +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +test. IN NS +SECTION ANSWER +test. IN NS ns.test +test. 3600 IN RRSIG NS 8 1 3600 20201116135527 20201019135527 1444 test. RGCxIO32TbbLTk6xZmTr+fjYPH50hntBxeOQ2DIj2pDsmjALcHYtVkOfpfk2EhOhHZd+9PLuoJPbJh6a9NqLSFeBvr0XZoCZoQ2g0tCHUNHcH5EVjA2TuYBQem6DVYnPLJ3914aRx0uA1j42b8dC2xsam/XkOo7U+dLbUW2Os1s= +SECTION ADDITIONAL +ns.test. IN A 1.2.3.5 +ns.test. 3600 IN RRSIG A 8 2 3600 20201116135527 20201019135527 1444 test. GskCc4/k6GjH9V9Jz2V5L2XLiizbOeWkB0feSbf+aN859S3vxVvtuqkvIgwY4LafUO1QAn/pUcv9zA7rcFO++rlg+8t6gvZTo9p3v0bfeIv2uJDsfSBD5jDh0WXlxjekfnrKrQp7zE+GiA93tWwKUWKPvxXDgP+n886e6WcbHJw= +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +ns.test. IN A +SECTION ANSWER +ns.test. IN A 1.2.3.5 +ns.test. 3600 IN RRSIG A 8 2 3600 20201116135527 20201019135527 1444 test. GskCc4/k6GjH9V9Jz2V5L2XLiizbOeWkB0feSbf+aN859S3vxVvtuqkvIgwY4LafUO1QAn/pUcv9zA7rcFO++rlg+8t6gvZTo9p3v0bfeIv2uJDsfSBD5jDh0WXlxjekfnrKrQp7zE+GiA93tWwKUWKPvxXDgP+n886e6WcbHJw= +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +ns.test. IN AAAA +SECTION AUTHORITY +test. 3600 IN SOA ns.test. host.test. 20201 3600 1800 604800 3600 +test. 3600 IN RRSIG SOA 8 1 3600 20201116135527 20201019135527 1444 test. IZJIDmEgf0W7A5G7hvvZ2hUqJ9Trbv1/i7ySapDmPbYV9lVCmHHobySxO01yDhI2/Pvpsvxqrm1Tiv3BxH8uzZ4keKgiQjBsSy4htAsFct9I4E7ly2glPj/Fm3oun3PsjJDv5QYhx0KS7w4IQKU7Nc9pfJc92uoUI5bdoC1pRGw= +ns.test. 3600 IN NSEC nz.test. A RRSIG +ns.test. 3600 IN RRSIG NSEC 8 2 3600 20201116135527 20201019135527 1444 test. PElArVB3KPg8KHAP7lzcNbhFuXNxTsHNTn1dZVncB5qmWRdIaeKpaXDjpH0JSXMaelGFS+/QhuQ6Hmw9+4VyZFRqMzGhw4agUR/2bxABHcDIG4ZpUwyeSP61ATTfHUkQVxaH2wjCWI/tfmesdP2xVE4GXyUvCIBxU914MkZbULU= +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +test. IN DNSKEY +SECTION ANSWER +test. 3600 IN DNSKEY 257 3 8 AwEAAbd9WqjzE2Pynz21OG5doSf9hFzMr5dhzz2waZ3vTa+0o5r7AjTAqmA1yH/B3+aAMihUm5ucZSfVqo7+kOaRE8yFj9aivOmA1n1+JLevJq/oyvQyjxQN2Qb89LyaNUT5oKZIiL+uyyhNW3KDR3SSbQ/GBwQNDHVcZi+JDR3RC0r7 ;{id = 1444 (ksk), size = 1024b} +test. 3600 IN RRSIG DNSKEY 8 1 3600 20201116135527 20201019135527 1444 test. UmRMS4iG9NBBHZYOtpwFFcJgbEb5SfHSgHd9XRe/8pTWM31WSDayn5ViPOBMqI1T5TXg2amc13dDI574xIM2oKMus3b5cBW72jJLW13jprBtslO6P8BMWb4HNnvLrJtQjwf3ErRirtTxinLmywQtmyr1cdthyG3Gp4N7i90fHSc= +SECTION ADDITIONAL +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qname qtype +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +example.test. IN DS +SECTION ANSWER +example.test. 3600 IN DS 55567 8 2 a2d578906330a10a57d40462257b6ce038bad3f7bf4a45c46c46086e20a94b39 +example.test. 3600 IN RRSIG DS 8 2 3600 20201116135527 20201019135527 1444 test. P7+FTYW2qHuJ4I1YbuvseEz5X1lOYAraGEHB3C5y0OOCQFmhmSiFRdquNi2NlpcS6FXLdsE0EU+Bo1+0atTG4EkMWXbpF21lrtbB51BdsnlX4Mzc/o375fvjiOMwmF6wPCUaOUN62jrVrhsE/hedaVyDphDToqL17ETohwgUO2I= +ENTRY_END + +ENTRY_BEGIN +MATCH opcode subdomain +ADJUST copy_id copy_query +REPLY QR NOERROR +SECTION QUESTION +example.test. IN NS +SECTION AUTHORITY +example.test. IN NS ns.example.test. +example.test. 3600 IN DS 55567 8 2 a2d578906330a10a57d40462257b6ce038bad3f7bf4a45c46c46086e20a94b39 +example.test. 3600 IN RRSIG DS 8 2 3600 20201116135527 20201019135527 1444 test. P7+FTYW2qHuJ4I1YbuvseEz5X1lOYAraGEHB3C5y0OOCQFmhmSiFRdquNi2NlpcS6FXLdsE0EU+Bo1+0atTG4EkMWXbpF21lrtbB51BdsnlX4Mzc/o375fvjiOMwmF6wPCUaOUN62jrVrhsE/hedaVyDphDToqL17ETohwgUO2I= +SECTION ADDITIONAL +ns.example.test. IN A 1.2.3.4 +ENTRY_END +RANGE_END + +; ns.example.test. +RANGE_BEGIN 0 100 + ADDRESS 1.2.3.4 +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR NOERROR +SECTION QUESTION +example.test. IN NS +SECTION ANSWER +example.test. IN NS ns.example.test. +example.test. 3600 IN RRSIG NS 8 2 3600 20201116135527 20201019135527 55567 example.test. l1JT0wMlK0YI7/CWHzexf/k0iafUhCgN+BdgjBXIRXmSQNf4HDTiAkbcWL2/15qtnp12nQy9JeiTdSQ3vtPoHAJX4C5uTWaze4ms+Wrrf+n92sLCjacP9x50uuicH3URT6cKb1QCAPwlvlWxIlZjAMYFScSns7+C441NMJT8aE4= +SECTION ADDITIONAL +ns.example.test. IN A 1.2.3.4 +ns.example.test. 3600 IN RRSIG A 8 3 3600 20201116135527 20201019135527 55567 example.test. 2PWaVaccZFQgfPKXNsdEGYUVaashCAj1ZhBo9XRt5eQKUFvZcauBjMnXIuxZFyWeootn1fZGw6GuPI5W48Y0FDx38H6adprkFgQikso2Y64jDdDMWznSo38Z/XqP+U0+kq4vmwonvmEMpm7hKnNEXvhqGKyGzyBwb+CZVJ2L8Eo= +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +ns.example.test. IN A +SECTION ANSWER +ns.example.test. IN A 1.2.3.4 +ns.example.test. 3600 IN RRSIG A 8 3 3600 20201116135527 20201019135527 55567 example.test. 2PWaVaccZFQgfPKXNsdEGYUVaashCAj1ZhBo9XRt5eQKUFvZcauBjMnXIuxZFyWeootn1fZGw6GuPI5W48Y0FDx38H6adprkFgQikso2Y64jDdDMWznSo38Z/XqP+U0+kq4vmwonvmEMpm7hKnNEXvhqGKyGzyBwb+CZVJ2L8Eo= +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +ns.example.test. IN AAAA +SECTION AUTHORITY +example.test. 3600 IN SOA ns.example.test. host.example.test. 20301 3600 1800 604800 3600 +example.test. 3600 IN RRSIG SOA 8 2 3600 20201116135527 20201019135527 55567 example.test. 2UUkScBAN37fJpSrelhE8DotKvmOzj3q9wicaanCIaCv95DE4nQnePih5B+ek3FIRjB/Uv2+z4Ro5Uxy94XAnlK0rCkDLSa0U9U7KP0ytc88sevO0x1SCPAMoZoJO6JqHkv42pdh54WSz+Zb/D8npY0j/tksHe/uX+VQnMymgb8= +ns.example.test. 3600 IN NSEC nz.example.test. A RRSIG +example.test. 3600 IN SOA ns.example.test. host.example.test. 20301 3600 1800 604800 3600 +ENTRY_END + +; response to DNSKEY priming query +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR NOERROR +SECTION QUESTION +example.test. IN DNSKEY +SECTION ANSWER +example.test. 3600 IN DNSKEY 257 3 8 AwEAAdug/L739i0mgN2nuK/bhxu3wFn5Ud9nK2+XUmZQlPUEZUC5YZvm1rfMmEWTGBn87fFxEu/kjFZHJ55JLzqsbbpVHLbmKCTT2gYR2FV2WDKROGKuYbVkJIXdKAjJ0ONuK507NinYvlWXIoxHn22KAWOd9wKgSTNHBlmGkX+ts3hh ;{id = 55567 (ksk), size = 1024b} +example.test. 3600 IN RRSIG DNSKEY 8 2 3600 20201116135527 20201019135527 55567 example.test. IbWMC6quOuZFNPAVxQLqCJ9nLhindBo826rnLcg5yMgs9dGUSPOCXAfHTmbgJAUNs9HTFfrJWNvasnETs0UOpmEuifGwWdH1OlME7Gny4RL2QmITUFeMW81Jz1tiVQxFXl6yxT0jxOxvz+bqMHlrz+8IeWQXcO+GZTPu8ueq30g= +ENTRY_END + +; response to query of interest +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +example.test. IN NSEC +SECTION ANSWER +; normal record has next owner in example.test. +; example.test. IN NSEC b.example.test. SOA DNSKEY NS RRSIG NSEC +example.test. IN NSEC b.foo.test. NS SOA RRSIG NSEC DNSKEY +example.test. 3600 IN RRSIG NSEC 8 2 3600 20201116135527 20201019135527 55567 example.test. xxepzzIxsJURk4/eZrwcDm5jhQNHtf1OmnPuu3T/w8y5NWwzlgn/hL17xoI71dIgTJg2GAq97wxEUhp951jtGMCeLH2Dz5lDZXxQI4wf2Wl43u2mTBQFRagDwfAauFc6Z4FYI/biDZyYcylZ3A5Q6j6ifFnsgMTL+cP0UIEZBTQ= +ENTRY_END +RANGE_END + +; CD=1 query for type NSEC +STEP 1 QUERY +ENTRY_BEGIN +REPLY RD CD DO +SECTION QUESTION +example.test. IN NSEC +ENTRY_END + +STEP 10 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD CD RA DO NOERROR +SECTION QUESTION +example.test. IN NSEC +SECTION ANSWER +; The overreaching NSEC is removed by the scrubber. +ENTRY_END + +SCENARIO_END diff --git a/testdata/stop_nxdomain_minimised.rpl b/testdata/stop_nxdomain_minimised.rpl index 0de22edde..d6e881f59 100644 --- a/testdata/stop_nxdomain_minimised.rpl +++ b/testdata/stop_nxdomain_minimised.rpl @@ -7,6 +7,7 @@ server: val-override-date: "20070916134226" fake-sha1: yes trust-anchor-signaling: no + domain-insecure: "anotherexample.local" stub-zone: name: "." @@ -69,7 +70,7 @@ REPLY QR AA NOERROR SECTION QUESTION anotherexample.local. IN TXT SECTION ANSWER -anotherexample.local. 86400 IN TXT "should not resolve this" +anotherexample.local. 86400 IN TXT "stub works" ENTRY_END RANGE_END @@ -95,7 +96,7 @@ STEP 20 QUERY ENTRY_BEGIN REPLY RD SECTION QUESTION -anotherexample.local. IN TXT +anotherexample2.local. IN TXT ENTRY_END ; query should be answered using NXDOMAIN for local in cache @@ -104,9 +105,27 @@ ENTRY_BEGIN MATCH all REPLY QR RD RA NXDOMAIN SECTION QUESTION -anotherexample.local. IN TXT +anotherexample2.local. IN TXT SECTION AUTHORITY . 86400 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2010111601 1800 900 604800 86400 ENTRY_END +STEP 40 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +anotherexample.local. IN TXT +ENTRY_END + +; The stub stops going higher in the negative cache. +STEP 50 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA NOERROR +SECTION QUESTION +anotherexample.local. IN TXT +SECTION ANSWER +anotherexample.local. 86400 IN TXT "stub works" +ENTRY_END + SCENARIO_END diff --git a/validator/val_neg.c b/validator/val_neg.c index 5835fcbaf..63f380ad9 100644 --- a/validator/val_neg.c +++ b/validator/val_neg.c @@ -938,6 +938,10 @@ void val_neg_addreply(struct val_neg_cache* neg, struct reply_info* rep) continue; if(!dname_subdomain_c(rep->rrsets[i]->rk.dname, zone->name)) continue; + if(ntohs(rep->rrsets[i]->rk.type) == LDNS_RR_TYPE_NSEC && + !nsec_nextowner_subdomain(rep->rrsets[i], zone->name)) { + continue; /* nextowner not in zone */ + } /* insert NSEC into this zone's tree */ neg_insert_data(neg, zone, rep->rrsets[i]); } @@ -1022,6 +1026,10 @@ void val_neg_addreferral(struct val_neg_cache* neg, struct reply_info* rep, continue; if(!dname_subdomain_c(rep->rrsets[i]->rk.dname, zone->name)) continue; + if(ntohs(rep->rrsets[i]->rk.type) == LDNS_RR_TYPE_NSEC && + !nsec_nextowner_subdomain(rep->rrsets[i], zone->name)) { + continue; /* nextowner not in zone */ + } /* insert NSEC into this zone's tree */ neg_insert_data(neg, zone, rep->rrsets[i]); } @@ -1110,12 +1118,14 @@ grab_nsec(struct rrset_cache* rrset_cache, uint8_t* qname, size_t qname_len, * @param rrset_cache: rrset cache * @param now: to check ttl against * @param region: where to alloc result + * @param topname: do not look higher than this name, so that the + * result cannot be taken from a zone above the current trust anchor. * @return rrset or NULL */ static struct ub_packed_rrset_key* neg_find_nsec(struct val_neg_cache* neg_cache, uint8_t* qname, size_t qname_len, uint16_t qclass, struct rrset_cache* rrset_cache, time_t now, - struct regional* region) + struct regional* region, uint8_t* topname) { int labs; uint32_t flags; @@ -1133,6 +1143,11 @@ neg_find_nsec(struct val_neg_cache* neg_cache, uint8_t* qname, size_t qname_len, lock_basic_unlock(&neg_cache->lock); return NULL; } + if(topname && !dname_subdomain_c(zone->name, topname)) { + /* Reject NSEC not within trust anchor's bailiwick */ + lock_basic_unlock(&neg_cache->lock); + return NULL; + } /* NSEC only for now */ if(zone->nsec3_hash) { @@ -1430,7 +1445,7 @@ val_neg_getmsg(struct val_neg_cache* neg, struct query_info* qinfo, /* Get best available NSEC for qname */ nsec = neg_find_nsec(neg, qinfo->qname, qinfo->qname_len, qinfo->qclass, - rrset_cache, now, region); + rrset_cache, now, region, topname); /* Matching NSEC, use to generate No Data answer. Not creating answers * yet for No Data proven using wildcard. */ @@ -1510,7 +1525,7 @@ val_neg_getmsg(struct val_neg_cache* neg, struct query_info* qinfo, * proof */ if(!(wcrr = neg_find_nsec(neg, wc_qinfo.qname, wc_qinfo.qname_len, qinfo->qclass, - rrset_cache, now, region))) + rrset_cache, now, region, topname))) return NULL; nodata_wc = NULL; diff --git a/validator/val_sigcrypt.c b/validator/val_sigcrypt.c index 55e7f1266..4139cc1fe 100644 --- a/validator/val_sigcrypt.c +++ b/validator/val_sigcrypt.c @@ -1641,6 +1641,16 @@ dnskey_verify_rrset_sig(struct regional* region, sldns_buffer* buf, return sec_status_bogus; /* NSEC3 owner not b32.signer */ } } + /* NSEC, a next owner that is not under the signer is not allowed.*/ + if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_NSEC && + !nsec_nextowner_subdomain(rrset, signer)) { + verbose(VERB_QUERY, "verify: NSEC next owner overreaches signer name"); + *reason = "NSEC next owner overreaches signer name"; + if(reason_bogus) + *reason_bogus = LDNS_EDE_DNSSEC_BOGUS; + return sec_status_bogus; /* nextowner overreaching */ + } + sigblock = (unsigned char*)signer+signer_len; if(siglen < 2+18+signer_len+1) { verbose(VERB_QUERY, "verify: too short, no signature data"); diff --git a/validator/val_utils.c b/validator/val_utils.c index 9ff1c224d..e77f93f5a 100644 --- a/validator/val_utils.c +++ b/validator/val_utils.c @@ -1384,3 +1384,20 @@ int derive_cname_from_dname(struct ub_packed_rrset_key* cname, memmove(out+prefix_len, dname_target, dname_target_len); return 1; } + +int nsec_nextowner_subdomain(struct ub_packed_rrset_key* rrset, uint8_t* name) +{ + struct packed_rrset_data* d; + uint8_t* next; + size_t nextlen; + if(ntohs(rrset->rk.type) != LDNS_RR_TYPE_NSEC) + return 0; + d = (struct packed_rrset_data*)rrset->entry.data; + if(!d || d->count == 0) + return 0; + next = d->rr_data[0]+2; + nextlen = dname_valid(next, d->rr_len[0]-2); + if(nextlen == 0) + return 0; /* malformed */ + return dname_subdomain_c(next, name); +} diff --git a/validator/val_utils.h b/validator/val_utils.h index b44915a2a..43386edbf 100644 --- a/validator/val_utils.h +++ b/validator/val_utils.h @@ -452,4 +452,7 @@ int derive_cname_from_dname(struct ub_packed_rrset_key* cname, void rrsig_get_signer(uint8_t* data, size_t len, uint8_t** sname, size_t* slen); +/** See if the NSEC nextowner name is a subdomain of the name. */ +int nsec_nextowner_subdomain(struct ub_packed_rrset_key* rrset, uint8_t* name); + #endif /* VALIDATOR_VAL_UTILS_H */