public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/bind] f43: DNSSEC Validation Bypass via Out-of-Zone NSEC Next Field (CVE-2026-13321)
@ 2026-08-26 9:29
0 siblings, 0 replies; only message in thread
From: @ 2026-08-26 9:29 UTC (permalink / raw)
To: git-commits
A new commit has been pushed.
Repo : rpms/bind
Branch : f43
Commit : 660c264560d9a7578ceb304d286dbc5649de4a68
Author : Petr Menšík <pemensik@redhat.com>
Date : 2026-08-26T11:07:49+02:00
Stats : +726/-0 in 3 file(s)
URL : https://src.fedoraproject.org/rpms/bind/c/660c264560d9a7578ceb304d286dbc5649de4a68?branch=f43
Log:
DNSSEC Validation Bypass via Out-of-Zone NSEC Next Field (CVE-2026-13321)
[9.18] [CVE-2026-13321] sec: usr: Fix DNSSEC validation bypass via out-of-zone NSEC Next Field
A malicious zone with out-of-zone NSEC next owner names can cause a DNSSEC validating resolver to cache such record and, if `synth-from-dnssec` is enabled, to generate negative answers for any zone that is covered by the range.
ISC would like to thank Qifan Zhang of Palo Alto Networks for reporting the issue.
Closes isc-projects/bind9#5873
---
diff --git a/bind-9.18-CVE-2026-13321-test.patch b/bind-9.18-CVE-2026-13321-test.patch
new file mode 100644
index 0000000..df6ba04
--- /dev/null
+++ b/bind-9.18-CVE-2026-13321-test.patch
@@ -0,0 +1,445 @@
+From eaa35628f4a201049295a8944f4d28e8a1013199 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Ayd=C4=B1n=20Mercan?= <aydin@isc.org>
+Date: Wed, 6 May 2026 16:54:57 +0300
+Subject: [PATCH] Add system test for out-of-zone nsec dnssec bypass
+
+A malicious zone with out-of-zone NSEC entries can get a DNSSEC
+validating resolver's cache to cover the victim zone for non-existence
+and prevent nameserver queries without DNSSEC failure.
+
+Test for this case with an `evil.test` zone that tries to cover the
+`victim.test` zone.
+
+(cherry picked from commit 654f9773c0af59965c343bdfeb096b3dffe9dd53)
+(cherry picked from commit c969ad2c17b43dd999e358bfeb280d3df6fab822)
+---
+ .../system/dnssec_bypass/ns1/named.conf.j2 | 31 ++++
+ bin/tests/system/dnssec_bypass/ns1/root.db | 19 +++
+ bin/tests/system/dnssec_bypass/ns1/test.db | 23 +++
+ .../system/dnssec_bypass/ns2/named.conf.j2 | 26 +++
+ bin/tests/system/dnssec_bypass/ns2/victim.db | 18 +++
+ bin/tests/system/dnssec_bypass/ns3/evil.db | 23 +++
+ .../system/dnssec_bypass/ns3/named.conf.j2 | 26 +++
+ .../system/dnssec_bypass/ns4/named.conf.j2 | 35 ++++
+ .../system/dnssec_bypass/tests_bypass.py | 152 ++++++++++++++++++
+ 9 files changed, 353 insertions(+)
+ create mode 100644 bin/tests/system/dnssec_bypass/ns1/named.conf.j2
+ create mode 100644 bin/tests/system/dnssec_bypass/ns1/root.db
+ create mode 100644 bin/tests/system/dnssec_bypass/ns1/test.db
+ create mode 100644 bin/tests/system/dnssec_bypass/ns2/named.conf.j2
+ create mode 100644 bin/tests/system/dnssec_bypass/ns2/victim.db
+ create mode 100644 bin/tests/system/dnssec_bypass/ns3/evil.db
+ create mode 100644 bin/tests/system/dnssec_bypass/ns3/named.conf.j2
+ create mode 100644 bin/tests/system/dnssec_bypass/ns4/named.conf.j2
+ create mode 100644 bin/tests/system/dnssec_bypass/tests_bypass.py
+
+diff --git a/bin/tests/system/dnssec_bypass/ns1/named.conf.j2 b/bin/tests/system/dnssec_bypass/ns1/named.conf.j2
+new file mode 100644
+index 0000000000..59ced1831a
+--- /dev/null
++++ b/bin/tests/system/dnssec_bypass/ns1/named.conf.j2
+@@ -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.
++ */
++
++options {
++ port @PORT@;
++ pid-file "named.pid";
++ listen-on { 10.53.0.1; };
++ listen-on-v6 { none; };
++ recursion no;
++ dnssec-validation no;
++};
++
++zone "test." {
++ type primary;
++ file "test.db.signed";
++};
++
++zone "." {
++ type primary;
++ file "root.db.signed";
++};
+diff --git a/bin/tests/system/dnssec_bypass/ns1/root.db b/bin/tests/system/dnssec_bypass/ns1/root.db
+new file mode 100644
+index 0000000000..8d98a0456c
+--- /dev/null
++++ b/bin/tests/system/dnssec_bypass/ns1/root.db
+@@ -0,0 +1,19 @@
++; 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 3600
++. IN SOA a.nil. a.nil. 1 3600 600 86400 300
++. IN NS a.root-servers.nil.
++
++a.root-servers.nil. IN A 10.53.0.1
++
++test. IN NS ns1.test.
++ns1.test. IN A 10.53.0.1
+diff --git a/bin/tests/system/dnssec_bypass/ns1/test.db b/bin/tests/system/dnssec_bypass/ns1/test.db
+new file mode 100644
+index 0000000000..6efcd95e42
+--- /dev/null
++++ b/bin/tests/system/dnssec_bypass/ns1/test.db
+@@ -0,0 +1,23 @@
++; 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.
++
++$ORIGIN test.
++$TTL 3600
++
++@ IN SOA a a 1 3600 600 86400 300
++ IN NS ns1.test.
++ns1 IN A 10.53.0.1
++
++evil IN NS ns1.evil
++ns1.evil IN A 10.53.0.3
++
++victim IN NS ns1.victim
++ns1.victim IN A 10.53.0.2
+diff --git a/bin/tests/system/dnssec_bypass/ns2/named.conf.j2 b/bin/tests/system/dnssec_bypass/ns2/named.conf.j2
+new file mode 100644
+index 0000000000..e81cee7cac
+--- /dev/null
++++ b/bin/tests/system/dnssec_bypass/ns2/named.conf.j2
+@@ -0,0 +1,26 @@
++/*
++ * 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 {
++ port @PORT@;
++ pid-file "named.pid";
++ listen-on { 10.53.0.2; };
++ listen-on-v6 { none; };
++ recursion no;
++ dnssec-validation no;
++};
++
++zone "victim.test." {
++ type primary;
++ file "victim.db.signed";
++};
+diff --git a/bin/tests/system/dnssec_bypass/ns2/victim.db b/bin/tests/system/dnssec_bypass/ns2/victim.db
+new file mode 100644
+index 0000000000..edcc234322
+--- /dev/null
++++ b/bin/tests/system/dnssec_bypass/ns2/victim.db
+@@ -0,0 +1,18 @@
++; 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.
++
++$ORIGIN victim.test.
++$TTL 3600
++
++@ IN SOA ns1 hostmaster 1 3600 600 86400 2147483647
++ IN NS ns1
++
++ns1 IN A 10.53.0.2
+diff --git a/bin/tests/system/dnssec_bypass/ns3/evil.db b/bin/tests/system/dnssec_bypass/ns3/evil.db
+new file mode 100644
+index 0000000000..618f9d3e85
+--- /dev/null
++++ b/bin/tests/system/dnssec_bypass/ns3/evil.db
+@@ -0,0 +1,23 @@
++; 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.
++
++$ORIGIN evil.test.
++$TTL 300
++
++@ IN SOA ns1 hostmaster 1 3600 600 86400 300
++ IN NS ns1
++; Try to poison the victim zone in a resolver cache.
++; If admitted, the aggressive NSEC cache will accept a range such as
++; [evil.test, b.victim.test) and will cause the victim nameserver to
++; be never queried.
++ IN NSEC b.victim.test. NS SOA RRSIG NSEC DNSKEY
++
++ns1 IN A 10.53.0.3
+diff --git a/bin/tests/system/dnssec_bypass/ns3/named.conf.j2 b/bin/tests/system/dnssec_bypass/ns3/named.conf.j2
+new file mode 100644
+index 0000000000..17d3e18e4e
+--- /dev/null
++++ b/bin/tests/system/dnssec_bypass/ns3/named.conf.j2
+@@ -0,0 +1,26 @@
++/*
++ * 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 {
++ port @PORT@;
++ pid-file "named.pid";
++ listen-on { 10.53.0.3; };
++ listen-on-v6 { none; };
++ recursion no;
++ dnssec-validation no;
++};
++
++zone "evil.test." {
++ type primary;
++ file "evil.db.signed";
++};
+diff --git a/bin/tests/system/dnssec_bypass/ns4/named.conf.j2 b/bin/tests/system/dnssec_bypass/ns4/named.conf.j2
+new file mode 100644
+index 0000000000..039695d9b7
+--- /dev/null
++++ b/bin/tests/system/dnssec_bypass/ns4/named.conf.j2
+@@ -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.4;
++ notify-source 10.53.0.4;
++ transfer-source 10.53.0.4;
++ port @PORT@;
++ pid-file "named.pid";
++ listen-on { 10.53.0.4; };
++ listen-on-v6 { none; };
++ allow-transfer { any; };
++ recursion yes;
++ dnssec-validation yes;
++ synth-from-dnssec yes;
++};
++
++trust-anchors {
++ @root.domain@ @root.type@ @root.contents@;
++};
++
++zone "." {
++ type hint;
++ file "../../_common/root.hint";
++};
+diff --git a/bin/tests/system/dnssec_bypass/tests_bypass.py b/bin/tests/system/dnssec_bypass/tests_bypass.py
+new file mode 100644
+index 0000000000..c41bb7e016
+--- /dev/null
++++ b/bin/tests/system/dnssec_bypass/tests_bypass.py
+@@ -0,0 +1,152 @@
++# 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 datetime import datetime, timedelta, timezone
++
++import shutil
++
++from cryptography.hazmat.primitives.asymmetric import ec
++
++import dns.dnssec
++import dns.name
++import dns.rdataclass
++import dns.rdataset
++import dns.rdatatype
++import dns.rrset
++import dns.zone
++
++from isctest.run import EnvCmd
++
++import isctest
++
++TTL = 3600
++
++
++def bootstrap():
++ keygen = EnvCmd("KEYGEN", "-q -a ECDSA256")
++ signer = EnvCmd("SIGNER", "-S -g -O full")
++
++ def sign_regular_zone(ns: str, zone: str, database: str) -> isctest.kasp.Key:
++ isctest.log.info(f"{zone}: generate keys")
++ keygen(zone, cwd=ns).out.strip()
++ ksk = keygen(f"-f KSK {zone}", cwd=ns).out.strip()
++
++ isctest.log.info(f"{zone}: sign zone")
++ signer(f"-o {zone} {database}", cwd=ns)
++
++ if ns != "ns1":
++ shutil.copy(f"{ns}/dsset-{zone}", f"ns1/dsset-{zone}")
++ shutil.copy(f"{ns}/{ksk}.key", f"ns1/{ksk}.key")
++
++ return isctest.kasp.Key(ksk, keydir=ns)
++
++ # dnssec-signzone and `dns.dnssec.sign_zone` correctly disregard the invalid
++ # NSEC entries when signing the zone. However, for this test we actualy *want*
++ # to serve invalid yet signed zones. To accomplish this we sign the zone and then
++ # replace the correct entries with the faulty ones accompanied by its RRSIG.
++ #
++ # TODO(aydin): move this to `isctest` to sign broken zones
++ def sign_rogue_zone(ns: str, zone: str, database: str) -> None:
++ # Read zone.
++ origin = dns.name.from_text(zone)
++ data = dns.zone.from_file(f"{ns}/{database}", origin=origin, relativize=False)
++
++ # Get key for signing.
++ isctest.log.info(f"{zone}: generate keys")
++ private_key = ec.generate_private_key(ec.SECP256R1())
++ dnskey = dns.dnssec.make_dnskey(
++ public_key=private_key.public_key(),
++ algorithm=dns.dnssec.Algorithm.ECDSAP256SHA256,
++ flags=257,
++ )
++
++ # Sign zone.
++ isctest.log.info(f"{zone}: sign zone")
++ now = datetime.now(timezone.utc)
++ inception = now - timedelta(hours=1)
++ expiration = now + timedelta(days=30)
++
++ for name, node in data.nodes.items():
++ owner = name.derelativize(origin)
++ rdatasets = list(node.rdatasets)
++
++ for rdataset in rdatasets:
++ rrset = dns.rrset.RRset(owner, rdataset.rdclass, rdataset.rdtype)
++ rrset.update(rdataset)
++
++ rrsig = dns.dnssec.sign(
++ rrset=rrset,
++ private_key=private_key,
++ signer=origin,
++ dnskey=dnskey,
++ inception=inception,
++ expiration=expiration,
++ deterministic=False,
++ )
++
++ rdataset = dns.rdataset.Rdataset(rrset.rdclass, dns.rdatatype.RRSIG)
++ rdataset.add(rrsig, rrset.ttl)
++ node.replace_rdataset(rdataset)
++
++ # Sign DNSKEY RRset.
++ dnskey_rrset = dns.rrset.RRset(origin, dns.rdataclass.IN, dns.rdatatype.DNSKEY)
++ dnskey_rrset.add(dnskey, ttl=TTL)
++
++ apex_node = data.nodes[origin]
++ apex_node.replace_rdataset(dnskey_rrset)
++
++ rrsig = dns.dnssec.sign(
++ rrset=dnskey_rrset,
++ private_key=private_key,
++ signer=origin,
++ dnskey=dnskey,
++ inception=inception,
++ expiration=expiration,
++ deterministic=False,
++ )
++ rdataset = dns.rdataset.Rdataset(rrset.rdclass, dns.rdatatype.RRSIG)
++ rdataset.add(rrsig, dnskey_rrset.ttl)
++ apex_node.replace_rdataset(rdataset)
++
++ # Output zone.
++ data.to_file(f"{ns}/{database}.signed", relativize=False)
++
++ # Output DS.
++ ds = dns.dnssec.make_ds(name=origin, key=dnskey, algorithm="SHA256")
++ with open(f"ns1/dsset-{zone}", "w", encoding="utf-8") as f:
++ f.write(f"{zone} {TTL} IN DS {ds.to_text()}\n")
++
++ sign_rogue_zone("ns3", "evil.test.", "evil.db")
++ sign_regular_zone("ns2", "victim.test.", "victim.db")
++ sign_regular_zone("ns1", "test.", "test.db")
++ root_ksk = sign_regular_zone("ns1", ".", "root.db")
++
++ return {
++ "root": root_ksk.into_ta("static-key"),
++ }
++
++
++def test_out_of_zone_nsec(ns4):
++ isctest.log.info("trying to poison aggressive nsec cache")
++ msg = isctest.query.create("nx.evil.test", "A")
++ res = isctest.query.tcp(msg, ns4.ip)
++ isctest.check.noadflag(res)
++
++ isctest.log.info("query victim from recursive")
++ msg = isctest.query.create("victim.test", "SOA")
++ res = isctest.query.tcp(msg, ns4.ip, attempts=1)
++ isctest.check.noerror(res)
++ isctest.check.adflag(res)
++ isctest.check.rr_count_eq(res.answer, 2)
++
++ isctest.log.info("checking for query history on victim nameserver")
++ with open("ns2/named.run", "r", encoding="utf-8") as f:
++ assert "(victim.test): query 'victim.test/SOA/IN' approved" in f.read()
+--
+2.55.0
+
diff --git a/bind-9.18-CVE-2026-13321.patch b/bind-9.18-CVE-2026-13321.patch
new file mode 100644
index 0000000..8a09ff1
--- /dev/null
+++ b/bind-9.18-CVE-2026-13321.patch
@@ -0,0 +1,277 @@
+From 72967445f37a01d28b4ecb0e8f907e22fddd5087 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Ayd=C4=B1n=20Mercan?= <aydin@isc.org>
+Date: Thu, 7 May 2026 18:59:20 +0300
+Subject: [PATCH] Reject out-of-zone NSEC next owner names
+
+When verifying DNSSEC records, make sure that a next owner name of
+an NSEC record is a subdomain of the signer field.
+
+This follows the specification RFC 4034, section 4.1.1:
+
+ Owner names of RRsets for which the given zone is not authoritative
+ (such as glue records) MUST NOT be listed in the Next Domain Name
+ unless at least one authoritative RRset exists at the same owner
+ name.
+
+While the above paragraph is intended for glue records, it also
+applies to out-of-zone data.
+
+(cherry picked from commit 4065512d25b71605b9502bb69dfb903776d35aa9)
+(cherry picked from commit 058023c66f11d78590d4aa8c4f98946c4c965e21)
+
+change dns_nsec_requiredtypespresent to dns_nsec_is_legal
+
+Change `dns_nsec_requiredtypespresent` to `dns_nsec_is_legal` as a
+function for checking multiple NSEC validity rules.
+
+Currently we now additionally check for out-of-zone NSEC entries.
+
+(cherry picked from commit be2a6a497312469890b552907d039d2de0b44ccc)
+(cherry picked from commit f751e19a30d107f04c2f644aff9f8dab8fed03ab)
+---
+ lib/dns/dnssec.c | 13 ++++++++++
+ lib/dns/include/dns/dnssec.h | 6 +++++
+ lib/dns/include/dns/nsec.h | 18 ++++++++++----
+ lib/dns/nsec.c | 17 ++++++++++---
+ lib/dns/resolver.c | 48 ++++++++++++++++++++++++++++++++++--
+ lib/ns/query.c | 6 ++---
+ 6 files changed, 94 insertions(+), 14 deletions(-)
+
+diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c
+index 9b9b1f2bb2..5acaea9ecb 100644
+--- a/lib/dns/dnssec.c
++++ b/lib/dns/dnssec.c
+@@ -357,8 +357,10 @@ isc_result_t
+ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
+ bool ignoretime, unsigned int maxbits, isc_mem_t *mctx,
+ dns_rdata_t *sigrdata, dns_name_t *wild) {
++ dns_rdata_nsec_t nsec;
+ dns_rdata_rrsig_t sig;
+ dns_fixedname_t fnewname;
++ dns_rdata_t rdata = DNS_RDATA_INIT;
+ isc_region_t r;
+ isc_buffer_t envbuf;
+ dns_rdata_t *rdatas;
+@@ -464,6 +466,17 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
+ }
+ break;
+ }
++ /*
++ * Check for out of zone NSEC entries.
++ */
++ if (set->type == dns_rdatatype_nsec) {
++ RETERR(dns_rdataset_first(set));
++ dns_rdataset_current(set, &rdata);
++ RETERR(dns_rdata_tostruct(&rdata, &nsec, NULL));
++ if (!dns_name_issubdomain(&nsec.next, &sig.signer)) {
++ return DNS_R_NOVALIDNSEC;
++ }
++ }
+
+ again:
+ ret = dst_context_create(key, mctx, DNS_LOGCATEGORY_DNSSEC, false,
+diff --git a/lib/dns/include/dns/dnssec.h b/lib/dns/include/dns/dnssec.h
+index cb8fd9dc20..2be11b9144 100644
+--- a/lib/dns/include/dns/dnssec.h
++++ b/lib/dns/include/dns/dnssec.h
+@@ -151,6 +151,9 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
+ * this record, as this requires a resolver or database.
+ * If 'ignoretime' is true, temporal validity will not be checked.
+ *
++ * If 'set' is of type NSEC, this function also verifies that the
++ * Next Name is a subdomain of the Signer's Name from 'sigrdata'.
++ *
+ * 'maxbits' specifies the maximum number of rsa exponent bits accepted.
+ *
+ * Requires:
+@@ -173,6 +176,9 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
+ *\li #DNS_R_KEYUNAUTHORIZED - the key cannot sign this data (either
+ * it is not a zone key or its flags prevent
+ * authentication)
++ *
++ *\li #DNS_R_NOVALIDNSEC - the NSEC rdata is not valid
++ *\li #DNS_R_KEYUNAUTHORIZED - the key cannot sign this data
+ *\li DST_R_*
+ */
+
+diff --git a/lib/dns/include/dns/nsec.h b/lib/dns/include/dns/nsec.h
+index 50df8e45f1..1e71bf14e8 100644
+--- a/lib/dns/include/dns/nsec.h
++++ b/lib/dns/include/dns/nsec.h
+@@ -119,13 +119,21 @@ dns_nsec_noexistnodata(dns_rdatatype_t type, const dns_name_t *name,
+ */
+
+ bool
+-dns_nsec_requiredtypespresent(dns_rdataset_t *rdataset);
+-/*
+- * Return true if all the NSEC records in rdataset have both
+- * NSEC and RRSIG present.
++dns_nsec_is_legal(dns_rdataset_t *rdataset, const dns_name_t *name);
++/**<
++ * \brief
++ * Validates a rdataset of type NSEC.
+ *
+- * Requires:
++ * This functions checks for the following in the given rdataset:
++ * \li All NSEC records have both NSEC and RRSIG present
++ * \li All NSEC entries are under the `name`
++ *
++ * \par Requires:
+ * \li rdataset to be a NSEC rdataset.
++ * \li `name` is a valid dns_name_t
++ *
++ * \retval true if all the checks pass
++ * \retval false otherwise
+ */
+
+ ISC_LANG_ENDDECLS
+diff --git a/lib/dns/nsec.c b/lib/dns/nsec.c
+index 80ee8d7d58..5abcce5f7f 100644
+--- a/lib/dns/nsec.c
++++ b/lib/dns/nsec.c
+@@ -21,6 +21,7 @@
+ #include <isc/util.h>
+
+ #include <dns/db.h>
++#include <dns/name.h>
+ #include <dns/nsec.h>
+ #include <dns/rdata.h>
+ #include <dns/rdatalist.h>
+@@ -497,8 +498,9 @@ dns_nsec_noexistnodata(dns_rdatatype_t type, const dns_name_t *name,
+ }
+
+ bool
+-dns_nsec_requiredtypespresent(dns_rdataset_t *nsecset) {
+- dns_rdataset_t rdataset;
++dns_nsec_is_legal(dns_rdataset_t *nsecset, const dns_name_t *name) {
++ dns_rdataset_t rdataset = DNS_RDATASET_INIT;
++ dns_rdata_nsec_t nsec;
+ isc_result_t result;
+ bool found = false;
+
+@@ -513,12 +515,19 @@ dns_nsec_requiredtypespresent(dns_rdataset_t *nsecset) {
+ {
+ dns_rdata_t rdata = DNS_RDATA_INIT;
+ dns_rdataset_current(&rdataset, &rdata);
+- if (!dns_nsec_typepresent(&rdata, dns_rdatatype_nsec) ||
+- !dns_nsec_typepresent(&rdata, dns_rdatatype_rrsig))
++
++ /* must never fail */
++ result = dns_rdata_tostruct(&rdata, &nsec, NULL);
++ INSIST(result == ISC_R_SUCCESS);
++
++ if (!dns_name_issubdomain(&nsec.next, name) ||
++ !dns_nsec_typepresent(&rdata, dns_rdatatype_rrsig) ||
++ !dns_nsec_typepresent(&rdata, dns_rdatatype_nsec))
+ {
+ dns_rdataset_disassociate(&rdataset);
+ return false;
+ }
++
+ found = true;
+ }
+ dns_rdataset_disassociate(&rdataset);
+diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c
+index 059ce53a9e..eac67f7a19 100644
+--- a/lib/dns/resolver.c
++++ b/lib/dns/resolver.c
+@@ -65,7 +65,9 @@
+ #include <dns/rootns.h>
+ #include <dns/stats.h>
+ #include <dns/tsig.h>
++#include <dns/types.h>
+ #include <dns/validator.h>
++#include <dns/view.h>
+ #include <dns/zone.h>
+
+ /* Detailed logging of fctx attach/detach */
+@@ -5603,6 +5605,36 @@ fctx_setresult(fetchctx_t *fctx, dns_rdataset_t *rdataset) {
+ return result;
+ }
+
++static bool
++get_and_check_signer_name(dns_name_t *signer, dns_rdataset_t *sigrdataset) {
++ dns_rdata_rrsig_t rrsig;
++ isc_result_t result;
++ dns_rdata_t rdata;
++
++ if (dns_rdataset_first(sigrdataset) != ISC_R_SUCCESS) {
++ return false;
++ }
++
++ rdata = (dns_rdata_t)DNS_RDATA_INIT;
++ dns_rdataset_current(sigrdataset, &rdata);
++ result = dns_rdata_tostruct(&rdata, &rrsig, NULL);
++ INSIST(result == ISC_R_SUCCESS);
++ dns_name_copy(&rrsig.signer, signer);
++
++ while (dns_rdataset_next(sigrdataset) == ISC_R_SUCCESS) {
++ rdata = (dns_rdata_t)DNS_RDATA_INIT;
++ dns_rdataset_current(sigrdataset, &rdata);
++ result = dns_rdata_tostruct(&rdata, &rrsig, NULL);
++ INSIST(result == ISC_R_SUCCESS);
++
++ if (!dns_name_equal(signer, &rrsig.signer)) {
++ return false;
++ }
++ }
++
++ return true;
++}
++
+ /*
+ * The validator has finished.
+ */
+@@ -5633,6 +5665,8 @@ validated(isc_task_t *task, isc_event_t *event) {
+ dns_fixedname_t fwild;
+ dns_name_t *wild = NULL;
+ dns_message_t *message = NULL;
++ dns_fixedname_t fsigner;
++ dns_name_t *signer = NULL;
+
+ UNUSED(task); /* for now */
+
+@@ -6021,10 +6055,20 @@ answer_response:
+ }
+
+ /*
+- * Don't cache NSEC if missing NSEC or RRSIG types.
++ * Don't cache if all the RRSIGs don't have the same
++ * signer.
++ */
++ signer = dns_fixedname_initname(&fsigner);
++ if (!get_and_check_signer_name(signer, sigrdataset)) {
++ continue;
++ }
++
++ /*
++ * Don't cache NSEC if missing NSEC or RRSIG
++ * types.
+ */
+ if (rdataset->type == dns_rdatatype_nsec &&
+- !dns_nsec_requiredtypespresent(rdataset))
++ !dns_nsec_is_legal(rdataset, signer))
+ {
+ continue;
+ }
+diff --git a/lib/ns/query.c b/lib/ns/query.c
+index 2a2ba1daba..8bbcd7ff30 100644
+--- a/lib/ns/query.c
++++ b/lib/ns/query.c
+@@ -10370,10 +10370,10 @@ query_coveringnsec(query_ctx_t *qctx) {
+ }
+
+ /*
+- * If NSEC or RRSIG are missing from the type map
+- * reject the NSEC RRset.
++ * Check that the NSEC entry is legal.
++ * (NSEC + RRSIG present and the entry isn't out-of-zone)
+ */
+- if (!dns_nsec_requiredtypespresent(qctx->rdataset)) {
++ if (!dns_nsec_is_legal(qctx->rdataset, signer)) {
+ goto cleanup;
+ }
+
+--
+2.55.0
+
diff --git a/bind.spec b/bind.spec
index 4e03c1e..1fb67d4 100644
--- a/bind.spec
+++ b/bind.spec
@@ -166,6 +166,9 @@ Patch47: bind-9.18-CVE-2026-10723-test.patch
# https://gitlab.isc.org/isc-projects/bind9/commit/b9ff2c9a36bb678fd1393d4f932b35a6882cd2a8
Patch48: bind-9.18-CVE-2026-13204.patch
Patch49: bind-9.18-CVE-2026-13204-test.patch
+# https://gitlab.isc.org/isc-projects/bind9/commit/36f3d50f9c8ebc8d25ee033e707ca502e20b083f
+Patch50: bind-9.18-CVE-2026-13321.patch
+Patch51: bind-9.18-CVE-2026-13321-test.patch
%{?systemd_ordering}
# https://fedoraproject.org/wiki/Changes/RPMSuportForSystemdSysusers
@@ -974,6 +977,7 @@ fi;
- Cache poisoning via label count discrepancy, RRSIG, wildcards (CVE-2026-11721)
- Incorrect acceptance of NSEC3 records (CVE-2026-10723)
- Unexpected exit with NSEC and NSEC3 both present (CVE-2026-13204)
+- DNSSEC Validation Bypass via Out-of-Zone NSEC Next Field (CVE-2026-13321)
* Wed Jun 17 2026 Petr Menšík <pemensik@redhat.com> - 32:9.18.50-1
- Update to 9.18.50 (rhbz#2489833)
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-08-26 9:29 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-26 9:29 [rpms/bind] f43: DNSSEC Validation Bypass via Out-of-Zone NSEC Next Field (CVE-2026-13321)
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox