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