commit 2e639c2555e1f1bb2d618df4f2be9131b2aa53ee
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/types.h b/lib/dns/include/dns/types.h
index 641d81fbb2..6f406297e5 100644
--- a/lib/dns/include/dns/types.h
+++ b/lib/dns/include/dns/types.h
@@ -356,6 +356,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/validator.c b/lib/dns/validator.c
index 243b19f64e..18df75da22 100644
--- a/lib/dns/validator.c
+++ b/lib/dns/validator.c
@@ -251,12 +251,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;
@@ -284,7 +297,7 @@ isdelegation(dns_name_t *name, dns_rdataset_t *rdataset,
 			goto trynsec3;
 		}
 		if (result != ISC_R_SUCCESS) {
-			return (false);
+			return (ISC_R_NOTFOUND);
 		}
 	}
 
@@ -298,7 +311,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:
 	/*
@@ -334,6 +347,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);
@@ -345,7 +370,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;
@@ -361,12 +386,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);
 }
 
 /*%
@@ -580,8 +605,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
@@ -741,10 +768,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");
@@ -1464,6 +1494,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:
@@ -2391,6 +2428,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,
@@ -2501,11 +2549,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;
@@ -2554,7 +2600,8 @@ validate_nx(dns_validator_t *val, bool resume) {
 			result = findnsec3proofs(val);
 			if (result == DNS_R_NSEC3ITERRANGE) {
 				validator_log(val, ISC_LOG_DEBUG(3),
-					      "too many iterations");
+					      "%s: too many iterations",
+					      __func__);
 				markanswer(val, "validate_nx (3)", NULL);
 				return (ISC_R_SUCCESS);
 			}
@@ -2590,7 +2637,7 @@ validate_nx(dns_validator_t *val, bool resume) {
 		result = findnsec3proofs(val);
 		if (result == DNS_R_NSEC3ITERRANGE) {
 			validator_log(val, ISC_LOG_DEBUG(3),
-				      "too many iterations");
+				      "%s: too many iterations", __func__);
 			markanswer(val, "validate_nx (4)", NULL);
 			return (ISC_R_SUCCESS);
 		}
@@ -2807,7 +2854,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);
