commit c908aaf8299b4d05b8ad3e76d5234fcb82ce4eae
Author: Michał Kępień <michal@isc.org>
Date:   Wed Mar 25 10:15:37 2026 +0100

    [9.16] [CVE-2026-1519] sec: usr: Fix unbounded NSEC3 iterations when validating referrals to unsigned delegations
    
    DNSSEC-signed zones may contain high iteration-count NSEC3 records,
    which prove that certain delegations are insecure. Previously, a
    validating resolver encountering such a delegation processed these
    iterations up to the number given, which could be a maximum of 65,535.
    This has been addressed by introducing a processing limit, set at 150.
    Now, if such an NSEC3 record is encountered, the delegation will be
    treated as insecure.
    
    ISC would like to thank Samy Medjahed/Ap4sh for bringing this
    vulnerability to our attention.
    
    Closes isc-projects/bind9#5708
    
    Backport of MR !935
    
    Merge branch '5708-confidential-nsec3-delegation-iteration-fix-fallback-to-insecure-9.16' into 'bind-9.16-release'
    
    See merge request isc-private/bind9!955

diff --git a/lib/dns/include/dns/nsec3.h b/lib/dns/include/dns/nsec3.h
index 5608515e9b..b8c6384f31 100644
--- a/lib/dns/include/dns/nsec3.h
+++ b/lib/dns/include/dns/nsec3.h
@@ -23,7 +23,8 @@
 #include <dns/rdatastruct.h>
 #include <dns/types.h>
 
-#define DNS_NSEC3_SALTSIZE 255
+#define DNS_NSEC3_SALTSIZE      255
+#define DNS_NSEC3_MAXITERATIONS 150U
 
 /*
  * hash = 1, flags =1, iterations = 2, salt length = 1, salt = 255 (max)
diff --git a/lib/dns/include/dns/result.h b/lib/dns/include/dns/result.h
index a67257a214..2951964af3 100644
--- a/lib/dns/include/dns/result.h
+++ b/lib/dns/include/dns/result.h
@@ -155,8 +155,9 @@
 #define DNS_R_TOOMANYRECORDS	(ISC_RESULTCLASS_DNS + 117)
 #define DNS_R_VERIFYFAILURE	(ISC_RESULTCLASS_DNS + 118)
 #define DNS_R_ATZONETOP		(ISC_RESULTCLASS_DNS + 119)
+#define DNS_R_NSEC3ITERRANGE	(ISC_RESULTCLASS_DNS + 120)
 
-#define DNS_R_NRESULTS 120 /*%< Number of results */
+#define DNS_R_NRESULTS 121 /*%< Number of results */
 
 /*
  * DNS wire format rcodes.
diff --git a/lib/dns/include/dns/types.h b/lib/dns/include/dns/types.h
index e20e1e505c..422ba2c6a7 100644
--- a/lib/dns/include/dns/types.h
+++ b/lib/dns/include/dns/types.h
@@ -350,6 +350,7 @@ enum {
 	((x) == dns_trust_additional || (x) == dns_trust_pending_additional)
 #define DNS_TRUST_GLUE(x)   ((x) == dns_trust_glue)
 #define DNS_TRUST_ANSWER(x) ((x) == dns_trust_answer)
+#define DNS_TRUST_SECURE(x) ((x) >= dns_trust_secure)
 
 /*%
  * Name checking severities.
diff --git a/lib/dns/result.c b/lib/dns/result.c
index 69445ad4c1..091327b061 100644
--- a/lib/dns/result.c
+++ b/lib/dns/result.c
@@ -165,6 +165,8 @@ static const char *text[DNS_R_NRESULTS] = {
 	"too many records",	    /*%< 117 DNS_R_TOOMANYRECORDS */
 	"verify failure",	    /*%< 118 DNS_R_VERIFYFAILURE */
 	"at top of zone",	    /*%< 119 DNS_R_ATZONETOP */
+
+	"NSEC3 iterations out of range", /*%< 120 DNS_R_NSEC3ITERRANGE */
 };
 
 static const char *ids[DNS_R_NRESULTS] = {
@@ -292,6 +294,7 @@ static const char *ids[DNS_R_NRESULTS] = {
 	"DNS_R_TOOMANYRECORDS",
 	"DNS_R_VERIFYFAILURE",
 	"DNS_R_ATZONETOP",
+	"DNS_R_NSEC3ITERRANGE",
 };
 
 static const char *rcode_text[DNS_R_NRCODERESULTS] = {
diff --git a/lib/dns/validator.c b/lib/dns/validator.c
index 3267123902..87d387b972 100644
--- a/lib/dns/validator.c
+++ b/lib/dns/validator.c
@@ -249,12 +249,25 @@ exit_check(dns_validator_t *val) {
 }
 
 /*%
- * Look in the NSEC record returned from a DS query to see if there is
- * a NS RRset at this name.  If it is found we are at a delegation point.
+ * The isdelegation() function is called as part of seeking the DS record.
+ * Look in the NSEC or NSEC3 record returned from a DS query to see if the
+ * record has the NS bitmap set. If so, we are at a delegation point.
+ *
+ * If the response contains NSEC3 records with too high iterations, we cannot
+ * (or rather we are not going to) validate the insecurity proof. Instead we
+ * are going to treat the message as insecure and just assume the DS was at
+ * the delegation.
+ *
+ * Returns:
+ *\li	#ISC_R_SUCCESS	the NS bitmap was set in the NSEC or NSEC3 record, or
+ *			the NSEC3 covers the name (in case of opt-out), or
+ *			we cannot validate the insecurity proof and are going
+ *			to treat the message as isnecure.
+ *\li	#ISC_R_NOTFOUND the NS bitmap was not set,
  */
-static bool
-isdelegation(dns_name_t *name, dns_rdataset_t *rdataset,
-	     isc_result_t dbresult) {
+static isc_result_t
+isdelegation(dns_validator_t *val, dns_name_t *name, dns_rdataset_t *rdataset,
+	     isc_result_t dbresult, const char *caller) {
 	dns_fixedname_t fixed;
 	dns_label_t hashlabel;
 	dns_name_t nsec3name;
@@ -282,7 +295,7 @@ isdelegation(dns_name_t *name, dns_rdataset_t *rdataset,
 			goto trynsec3;
 		}
 		if (result != ISC_R_SUCCESS) {
-			return (false);
+			return (ISC_R_NOTFOUND);
 		}
 	}
 
@@ -296,7 +309,7 @@ isdelegation(dns_name_t *name, dns_rdataset_t *rdataset,
 		dns_rdata_reset(&rdata);
 	}
 	dns_rdataset_disassociate(&set);
-	return (found);
+	return (found ? ISC_R_SUCCESS : ISC_R_NOTFOUND);
 
 trynsec3:
 	/*
@@ -332,6 +345,18 @@ trynsec3:
 			if (nsec3.hash != 1) {
 				continue;
 			}
+			/*
+			 * If there are too many iterations assume bad things
+			 * are happening and bail out early. Treat as if the
+			 * DS was at the delegation.
+			 */
+			if (nsec3.iterations > DNS_NSEC3_MAXITERATIONS) {
+				validator_log(val, ISC_LOG_DEBUG(3),
+					      "%s: too many iterations",
+					      caller);
+				dns_rdataset_disassociate(&set);
+				return (ISC_R_SUCCESS);
+			}
 			length = isc_iterated_hash(
 				hash, nsec3.hash, nsec3.iterations, nsec3.salt,
 				nsec3.salt_length, name->ndata, name->length);
@@ -343,7 +368,7 @@ trynsec3:
 				found = dns_nsec3_typepresent(&rdata,
 							      dns_rdatatype_ns);
 				dns_rdataset_disassociate(&set);
-				return (found);
+				return (found ? ISC_R_SUCCESS : ISC_R_NOTFOUND);
 			}
 			if ((nsec3.flags & DNS_NSEC3FLAG_OPTOUT) == 0) {
 				continue;
@@ -359,12 +384,12 @@ trynsec3:
 			      memcmp(hash, nsec3.next, length) < 0)))
 			{
 				dns_rdataset_disassociate(&set);
-				return (true);
+				return (ISC_R_SUCCESS);
 			}
 		}
 		dns_rdataset_disassociate(&set);
 	}
-	return (found);
+	return (found ? ISC_R_SUCCESS : ISC_R_NOTFOUND);
 }
 
 /*%
@@ -569,8 +594,10 @@ fetch_callback_ds(isc_task_t *task, isc_event_t *event) {
 		} else if (eresult == DNS_R_SERVFAIL) {
 			goto unexpected;
 		} else if (eresult != DNS_R_CNAME &&
-			   isdelegation(dns_fixedname_name(&devent->foundname),
-					&val->frdataset, eresult))
+			   isdelegation(val,
+					dns_fixedname_name(&devent->foundname),
+					&val->frdataset, eresult,
+					"fetch_callback_ds") == ISC_R_SUCCESS)
 		{
 			/*
 			 * Failed to find a DS while trying to prove
@@ -729,10 +756,13 @@ validator_callback_ds(isc_task_t *task, isc_event_t *event) {
 			      dns_trust_totext(val->frdataset.trust));
 		have_dsset = (val->frdataset.type == dns_rdatatype_ds);
 		name = dns_fixedname_name(&val->fname);
+
 		if ((val->attributes & VALATTR_INSECURITY) != 0 &&
 		    val->frdataset.covers == dns_rdatatype_ds &&
 		    NEGATIVE(&val->frdataset) &&
-		    isdelegation(name, &val->frdataset, DNS_R_NCACHENXRRSET))
+		    isdelegation(val, name, &val->frdataset,
+				 DNS_R_NCACHENXRRSET,
+				 "validator_callback_ds") == ISC_R_SUCCESS)
 		{
 			result = markanswer(val, "validator_callback_ds",
 					    "no DS and this is a delegation");
@@ -1427,6 +1457,13 @@ verify(dns_validator_t *val, dst_key_t *key, dns_rdata_t *rdata,
 	bool ignore = false;
 	dns_name_t *wild;
 
+	if (DNS_TRUST_SECURE(val->event->rdataset->trust)) {
+		/*
+		 * This RRset was already verified before.
+		 */
+		return ISC_R_SUCCESS;
+	}
+
 	val->attributes |= VALATTR_TRIEDVERIFY;
 	wild = dns_fixedname_initname(&fixed);
 again:
@@ -2330,6 +2367,17 @@ validate_neg_rrset(dns_validator_t *val, dns_name_t *name,
 		}
 	}
 
+	if (rdataset->type != dns_rdatatype_nsec &&
+	    DNS_TRUST_SECURE(rdataset->trust))
+	{
+		/*
+		 * The negative response data is already verified.
+		 * We skip NSEC records, because they require special
+		 * processing in validator_callback_nsec().
+		 */
+		return DNS_R_CONTINUE;
+	}
+
 	val->currentset = rdataset;
 	result = create_validator(val, name, rdataset->type, rdataset,
 				  sigrdataset, validator_callback_nsec,
@@ -2439,11 +2487,9 @@ validate_ncache(dns_validator_t *val, bool resume) {
 		}
 
 		result = validate_neg_rrset(val, name, rdataset, sigrdataset);
-		if (result == DNS_R_CONTINUE) {
-			continue;
+		if (result != DNS_R_CONTINUE) {
+			return (result);
 		}
-
-		return (result);
 	}
 	if (result == ISC_R_NOMORE) {
 		result = ISC_R_SUCCESS;
@@ -2489,7 +2535,14 @@ validate_nx(dns_validator_t *val, bool resume) {
 	 */
 	if (!NEEDNODATA(val) && !NEEDNOWILDCARD(val) && NEEDNOQNAME(val)) {
 		if (!FOUNDNOQNAME(val)) {
-			findnsec3proofs(val);
+			result = findnsec3proofs(val);
+			if (result == DNS_R_NSEC3ITERRANGE) {
+				validator_log(val, ISC_LOG_DEBUG(3),
+					      "%s: too many iterations",
+					      __func__);
+				markanswer(val, "validate_nx (3)", NULL);
+				return (ISC_R_SUCCESS);
+			}
 		}
 
 		if (FOUNDNOQNAME(val) && FOUNDCLOSEST(val) && !FOUNDOPTOUT(val))
@@ -2519,7 +2572,13 @@ validate_nx(dns_validator_t *val, bool resume) {
 	}
 
 	if (!FOUNDNOQNAME(val) && !FOUNDNODATA(val)) {
-		findnsec3proofs(val);
+		result = findnsec3proofs(val);
+		if (result == DNS_R_NSEC3ITERRANGE) {
+			validator_log(val, ISC_LOG_DEBUG(3),
+				      "%s: too many iterations", __func__);
+			markanswer(val, "validate_nx (4)", NULL);
+			return (ISC_R_SUCCESS);
+		}
 	}
 
 	/*
@@ -2733,7 +2792,9 @@ seek_ds(dns_validator_t *val, isc_result_t *resp) {
 			return (ISC_R_COMPLETE);
 		}
 
-		if (isdelegation(tname, &val->frdataset, result)) {
+		result = isdelegation(val, tname, &val->frdataset, result,
+				      "seek_ds");
+		if (result == ISC_R_SUCCESS) {
 			*resp = markanswer(val, "proveunsecure (4)",
 					   "this is a delegation");
 			return (ISC_R_COMPLETE);
