commit 388f9a0bae83f159c376c9871ddbefa0a1a32899
Author: Michał Kępień <michal@isc.org>
Date:   Wed Mar 25 10:16:02 2026 +0100

    [9.11] [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.11' into 'bind-9.11-release'
    
    See merge request isc-private/bind9!958

diff --git a/lib/dns/include/dns/nsec3.h b/lib/dns/include/dns/nsec3.h
index 07d2bf0848..74e474083c 100644
--- a/lib/dns/include/dns/nsec3.h
+++ b/lib/dns/include/dns/nsec3.h
@@ -24,7 +24,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 92fa5e1290..b609b81d95 100644
--- a/lib/dns/include/dns/result.h
+++ b/lib/dns/include/dns/result.h
@@ -153,8 +153,9 @@
 #define DNS_R_BADTSIG			(ISC_RESULTCLASS_DNS + 115)
 #define DNS_R_BADSIG0			(ISC_RESULTCLASS_DNS + 116)
 #define DNS_R_TOOMANYRECORDS		(ISC_RESULTCLASS_DNS + 117)
+#define DNS_R_NSEC3ITERRANGE		(ISC_RESULTCLASS_DNS + 118)
 
-#define DNS_R_NRESULTS			118	/*%< Number of results */
+#define DNS_R_NRESULTS			119	/*%< Number of results */
 
 /*
  * DNS wire format rcodes.
diff --git a/lib/dns/include/dns/types.h b/lib/dns/include/dns/types.h
index 567e8a879e..02ea7020c5 100644
--- a/lib/dns/include/dns/types.h
+++ b/lib/dns/include/dns/types.h
@@ -357,7 +357,7 @@ enum {
 					 (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 9bfc959831..4e5b00fe5d 100644
--- a/lib/dns/result.c
+++ b/lib/dns/result.c
@@ -165,6 +165,8 @@ static const char *text[DNS_R_NRESULTS] = {
 	"TSIG in wrong location",	       /*%< 115 DNS_R_BADTSIG */
 	"SIG(0) in wrong location",	       /*%< 116 DNS_R_BADSIG0 */
 	"too many records",	               /*%< 117 DNS_R_TOOMANYRECORDS */
+
+	"NSEC3 iterations out of range",       /*%< 120 DNS_R_NSEC3ITERRANGE */
 };
 
 static const char *ids[DNS_R_NRESULTS] = {
@@ -290,6 +292,7 @@ static const char *ids[DNS_R_NRESULTS] = {
 	"DNS_R_BADTSIG",
 	"DNS_R_BADSIG0",
 	"DNS_R_TOOMANYRECORDS",
+	"DNS_R_NSEC3ITERRANGE",
 };
 
 static const char *rcode_text[DNS_R_NRCODERESULTS] = {
diff --git a/lib/dns/validator.c b/lib/dns/validator.c
index d2d9d929ee..bcedc84a5d 100644
--- a/lib/dns/validator.c
+++ b/lib/dns/validator.c
@@ -262,12 +262,25 @@ dlv_algorithm_supported(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;
@@ -295,7 +308,7 @@ isdelegation(dns_name_t *name, dns_rdataset_t *rdataset,
 		if (result == ISC_R_NOTFOUND)
 			goto trynsec3;
 		if (result != ISC_R_SUCCESS)
-			return (false);
+			return (ISC_R_NOTFOUND);
 	}
 
 	INSIST(set.type == dns_rdatatype_nsec);
@@ -308,7 +321,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:
 	/*
@@ -345,18 +358,33 @@ isdelegation(dns_name_t *name, dns_rdataset_t *rdataset,
 			(void)dns_rdata_tostruct(&rdata, &nsec3, NULL);
 			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);
 			if (length != isc_buffer_usedlength(&buffer))
 				continue;
+
 			order = memcmp(hash, owner, length);
 			if (order == 0) {
 				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;
@@ -370,12 +398,12 @@ isdelegation(dns_name_t *name, dns_rdataset_t *rdataset,
 					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);
 }
 
 /*%
@@ -590,7 +618,8 @@ dsfetched2(isc_task_t *task, isc_event_t *event) {
 		 */
 		tname = dns_fixedname_name(&devent->foundname);
 		if (eresult != DNS_R_CNAME &&
-		    isdelegation(tname, &val->frdataset, eresult)) {
+		    isdelegation(val, tname, &val->frdataset, eresult,
+				 "dsfetched2") == ISC_R_SUCCESS) {
 			if (val->mustbesecure) {
 				validator_log(val, ISC_LOG_WARNING,
 					      "must be secure failure, no DS"
@@ -747,10 +776,13 @@ dsvalidated(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,
+				 "dsvalidated") == ISC_R_SUCCESS) {
 			if (val->mustbesecure) {
 				validator_log(val, ISC_LOG_WARNING,
 					      "must be secure failure, no DS "
@@ -1517,6 +1549,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:
@@ -2685,6 +2724,19 @@ validate_authority(dns_validator_t *val, bool resume) {
 							dns_rdatatype_soa))
 					continue;
 			}
+
+			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
+				 * authvalidated().
+				 */
+				continue;
+			}
+
 			val->currentset = rdataset;
 			result = create_validator(val, name, rdataset->type,
 						  rdataset, sigrdataset,
@@ -2762,6 +2814,18 @@ validate_ncache(dns_validator_t *val, bool resume) {
 						dns_rdatatype_soa))
 				continue;
 		}
+
+		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 authvalidated().
+			 */
+			continue;
+		}
+
 		val->currentset = rdataset;
 		result = create_validator(val, name, rdataset->type,
 					  rdataset, sigrdataset,
@@ -2809,8 +2873,17 @@ nsecvalidate(dns_validator_t *val, bool resume) {
 	 * had a secure wildcard answer.
 	 */
 	if (!NEEDNODATA(val) && !NEEDNOWILDCARD(val) && NEEDNOQNAME(val)) {
-		if (!FOUNDNOQNAME(val))
-			findnsec3proofs(val);
+		if (!FOUNDNOQNAME(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)");
+				return (ISC_R_SUCCESS);
+			}
+		}
+
 		if (FOUNDNOQNAME(val) && FOUNDCLOSEST(val) &&
 		    !FOUNDOPTOUT(val)) {
 			validator_log(val, ISC_LOG_DEBUG(3),
@@ -2836,8 +2909,15 @@ nsecvalidate(dns_validator_t *val, bool resume) {
 		return (DNS_R_NOVALIDNSEC);
 	}
 
-	if (!FOUNDNOQNAME(val) && !FOUNDNODATA(val))
-		findnsec3proofs(val);
+	if (!FOUNDNOQNAME(val) && !FOUNDNODATA(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)");
+			return (ISC_R_SUCCESS);
+		}
+	}
 
 	/*
 	 * Do we need to check for the wildcard?
@@ -3419,7 +3499,8 @@ proveunsecure(dns_validator_t *val, bool have_ds, bool resume)
 				result = DNS_R_NOVALIDSIG;
 				goto out;
 			}
-			if (isdelegation(tname, &val->frdataset, result)) {
+			if (isdelegation(val, tname, &val->frdataset, result,
+					 "proveunsecure") == ISC_R_SUCCESS) {
 				if (val->mustbesecure) {
 					validator_log(val, ISC_LOG_WARNING,
 						      "must be secure failure, "
