[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 50.
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

Merge branch '5708-confidential-nsec3-delegation-iteration-fix-fallback-to-insecure' into 'v9.21.20-release'

See merge request isc-private/bind9!935
This commit is contained in:
Michał Kępień 2026-03-13 13:16:28 +01:00
commit 779463a703
10 changed files with 333 additions and 33 deletions

View file

@ -0,0 +1,35 @@
/*
* Copyright (C) Internet Systems Consortium, Inc. ("ISC")
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* See the COPYRIGHT file distributed with this work for additional
* information regarding copyright ownership.
*/
options {
query-source address 10.53.0.1;
notify-source 10.53.0.1;
transfer-source 10.53.0.1;
port @PORT@;
pid-file "named.pid";
listen-on { 10.53.0.1; };
listen-on-v6 { none; };
recursion no;
dnssec-validation no;
};
controls {
inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
};
include "../../_common/rndc.key";
zone "." {
type primary;
file "root.db";
};

View file

@ -0,0 +1,25 @@
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
;
; SPDX-License-Identifier: MPL-2.0
;
; This Source Code Form is subject to the terms of the Mozilla Public
; License, v. 2.0. If a copy of the MPL was not distributed with this
; file, you can obtain one at https://mozilla.org/MPL/2.0/.
;
; See the COPYRIGHT file distributed with this work for additional
; information regarding copyright ownership.
$TTL 300
. IN SOA . . (
2025063000 ; serial
600 ; refresh
600 ; retry
1200 ; expire
600 ; minimum
)
. NS a.root-servers.nil.
a.root-servers.nil A 10.53.0.1
iter-too-many. NS ns2.iter-too-many.
ns2.iter-too-many. A 10.53.0.2

View file

@ -0,0 +1,31 @@
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
;
; SPDX-License-Identifier: MPL-2.0
;
; This Source Code Form is subject to the terms of the Mozilla Public
; License, v. 2.0. If a copy of the MPL was not distributed with this
; file, you can obtain one at https://mozilla.org/MPL/2.0/.
;
; See the COPYRIGHT file distributed with this work for additional
; information regarding copyright ownership.
{% raw %}
$TTL 300
@ IN SOA ns2.iter-too-many. hostmaster.iter-too-many. (
2026020300 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
@ IN NS ns2.iter-too-many.
ns2 IN A 10.53.0.2
sub IN NS ns2.sub.iter-too-many.
ns2.sub IN A 10.53.0.2
{% endraw %}
{% for dnskey in dnskeys %}
@dnskey@
{% endfor %}

View file

@ -0,0 +1,40 @@
/*
* Copyright (C) Internet Systems Consortium, Inc. ("ISC")
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* See the COPYRIGHT file distributed with this work for additional
* information regarding copyright ownership.
*/
options {
query-source address 10.53.0.2;
notify-source 10.53.0.2;
transfer-source 10.53.0.2;
port @PORT@;
pid-file "named.pid";
listen-on { 10.53.0.2; };
listen-on-v6 { none; };
recursion no;
dnssec-validation no;
};
controls {
inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
};
include "../../_common/rndc.key";
zone "iter-too-many" {
type primary;
file "iter-too-many.signed.db";
};
zone "sub.iter-too-many" {
type primary;
file "sub.iter-too-many.db";
};

View file

@ -0,0 +1,24 @@
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
;
; SPDX-License-Identifier: MPL-2.0
;
; This Source Code Form is subject to the terms of the Mozilla Public
; License, v. 2.0. If a copy of the MPL was not distributed with this
; file, you can obtain one at https://mozilla.org/MPL/2.0/.
;
; See the COPYRIGHT file distributed with this work for additional
; information regarding copyright ownership.
$TTL 300
@ IN SOA ns2.sub.iter-too-many. hostmaster.sub.iter-too-many. (
2026020300 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
@ IN NS ns2.sub.iter-too-many.
ns2 IN A 10.53.0.2
example IN A 127.0.0.1

View file

@ -0,0 +1,37 @@
/*
* Copyright (C) Internet Systems Consortium, Inc. ("ISC")
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* See the COPYRIGHT file distributed with this work for additional
* information regarding copyright ownership.
*/
options {
query-source address 10.53.0.3;
notify-source 10.53.0.3;
transfer-source 10.53.0.3;
port @PORT@;
pid-file "named.pid";
listen-on { 10.53.0.3; };
listen-on-v6 { none; };
recursion yes;
dnssec-validation yes;
};
controls {
inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
};
include "../../_common/rndc.key";
zone "." {
type hint;
file "../../_common/root.hint";
};
include "trusted.conf";

View file

@ -0,0 +1 @@
../../_common/trusted.conf.j2

View file

@ -0,0 +1,61 @@
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
#
# SPDX-License-Identifier: MPL-2.0
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
#
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
from isctest.run import EnvCmd
import isctest
def bootstrap():
templates = isctest.template.TemplateEngine(".")
keygen = EnvCmd("KEYGEN", "-a ECDSA256")
signer = EnvCmd("SIGNER")
isctest.log.info("setup iter-too-many.")
zonename = "iter-too-many."
ksk_name = keygen(f"-f KSK {zonename}", cwd="ns2").out.strip()
zsk_name = keygen(f"{zonename}", cwd="ns2").out.strip()
ksk = isctest.kasp.Key(ksk_name, keydir="ns2")
zsk = isctest.kasp.Key(zsk_name, keydir="ns2")
dnskeys = [ksk.dnskey, zsk.dnskey]
tdata = {
"dnskeys": dnskeys,
}
templates.render(f"ns2/{zonename}db", tdata, template=f"ns2/{zonename}db.j2.manual")
signer(
f"-P -o {zonename} -f {zonename}signed.db -3 A1B2C3D4 -H too-many -H 51 -S {zonename}db",
cwd="ns2",
)
return {
"trust_anchors": [
ksk.into_ta("static-key"),
],
}
def test_excessive_nsec3_iterations_delegation(ns3):
# reproducer for CVE-2026-1519 [GL#5708]
zone = "example.sub.iter-too-many"
msg = isctest.query.create(zone, "A")
res = isctest.query.tcp(msg, ns3.ip)
# an insecure response is expected regardless of the NSEC3 iteration limit,
# because the sub.iter-too-many. zone is unsigned. the real difference is
# in the CPU usage required for generating such response, but that can't be
# easily and reliably tested in an automated fashion
isctest.check.noerror(res)
with ns3.watch_log_from_start() as watcher:
watcher.wait_for_line(
f"validating {zone}/A: validator_callback_ds: too many iterations"
)

View file

@ -384,6 +384,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.

View file

@ -217,7 +217,8 @@ markanswer(dns_validator_t *val, const char *where) {
* Mark the RRsets in val->vstat with trust level secure.
*/
static void
marksecure(dns_validator_t *val) {
marksecure(dns_validator_t *val, const char *where) {
validator_log(val, ISC_LOG_DEBUG(3), "marking as secure (%s)", where);
dns_rdataset_settrust(val->rdataset, dns_trust_secure);
if (val->sigrdataset != NULL) {
dns_rdataset_settrust(val->sigrdataset, dns_trust_secure);
@ -244,12 +245,25 @@ validator_done(dns_validator_t *val, isc_result_t result) {
}
/*%
* 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;
@ -276,7 +290,7 @@ isdelegation(dns_name_t *name, dns_rdataset_t *rdataset,
goto trynsec3;
}
if (result != ISC_R_SUCCESS) {
return false;
return ISC_R_NOTFOUND;
}
}
@ -290,7 +304,7 @@ isdelegation(dns_name_t *name, dns_rdataset_t *rdataset,
found = dns_nsec_typepresent(&rdata, dns_rdatatype_ns);
}
dns_rdataset_disassociate(&set);
return found;
return found ? ISC_R_SUCCESS : ISC_R_NOTFOUND;
trynsec3:
/*
@ -325,6 +339,18 @@ trynsec3:
if (nsec3.next_length > NSEC3_MAX_HASH_LENGTH) {
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);
@ -336,7 +362,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;
@ -352,12 +378,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;
}
static void
@ -593,9 +619,10 @@ fetch_callback_ds(void *arg) {
break;
case DNS_R_NXRRSET:
case DNS_R_NCACHENXRRSET:
if (isdelegation(resp->foundname, &val->frdataset,
eresult))
{
result = isdelegation(val, resp->foundname,
&val->frdataset, eresult,
"fetch_callback_ds");
if (result == ISC_R_SUCCESS) {
/*
* Failed to find a DS while trying to prove
* insecurity. If this is a zone cut, that
@ -708,10 +735,13 @@ validator_callback_ds(void *arg) {
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");
} else if ((val->attributes & VALATTR_INSECURITY) != 0) {
@ -1441,11 +1471,19 @@ 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->rdataset->trust)) {
/*
* This RRset was already verified before.
*/
return ISC_R_SUCCESS;
}
val->attributes |= VALATTR_TRIEDVERIFY;
wild = dns_fixedname_initname(&fixed);
if (over_max_validations(val)) {
return ISC_R_QUOTA;
}
wild = dns_fixedname_initname(&fixed);
again:
result = dns_dnssec_verify(val->name, val->rdataset, key, ignore,
val->view->mctx, rdata, wild);
@ -1797,9 +1835,7 @@ validate_answer_finish(void *arg) {
}
if (val->result == ISC_R_SUCCESS) {
marksecure(val);
validator_log(val, ISC_LOG_DEBUG(3),
"marking as secure, noqname proof not needed");
marksecure(val, "noqname proof not needed");
validate_async_done(val, val->result);
return;
}
@ -2002,8 +2038,7 @@ validate_dnskey_dsset_done(dns_validator_t *val, isc_result_t result) {
/* Abort, abort, abort! */
break;
case ISC_R_SUCCESS:
marksecure(val);
validator_log(val, ISC_LOG_DEBUG(3), "marking as secure (DS)");
marksecure(val, "validate_dnskey (DS)");
break;
case ISC_R_NOMORE:
if (val->unsupported_algorithm != 0 ||
@ -2742,11 +2777,21 @@ 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->nxset = rdataset;
RETERR(create_validator(val, name, rdataset->type, rdataset,
sigrdataset, validator_callback_nsec,
"validate_neg_rrset"));
val->authcount++;
return DNS_R_WAIT;
}
@ -2849,11 +2894,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;
@ -2902,7 +2945,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)");
return ISC_R_SUCCESS;
}
@ -2910,9 +2954,7 @@ validate_nx(dns_validator_t *val, bool resume) {
if (FOUNDNOQNAME(val) && FOUNDCLOSEST(val) && !FOUNDOPTOUT(val))
{
validator_log(val, ISC_LOG_DEBUG(3),
"marking as secure, noqname proof found");
marksecure(val);
marksecure(val, "validate_nx (noqname proof found)");
return ISC_R_SUCCESS;
} else if (FOUNDOPTOUT(val) &&
dns_name_countlabels(
@ -2938,7 +2980,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)");
return ISC_R_SUCCESS;
}
@ -2963,7 +3005,8 @@ validate_nx(dns_validator_t *val, bool resume) {
validator_log(val, ISC_LOG_DEBUG(3),
"nonexistence proof(s) found");
if (val->message == NULL) {
marksecure(val);
marksecure(val,
"validate_nx (nonexistence proofs found)");
} else {
val->secure = true;
}
@ -3272,7 +3315,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, "seek_ds (3)");
return ISC_R_COMPLETE;
}