diff --git a/bind-9.18-CVE-2026-13321.patch b/bind-9.18-CVE-2026-13321.patch new file mode 100644 index 0000000..eae56b6 --- /dev/null +++ b/bind-9.18-CVE-2026-13321.patch @@ -0,0 +1,766 @@ +From 384331c4c560e658ce5349afc7e556e939f5301e Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ayd=C4=B1n=20Mercan?= +Date: Wed, 6 May 2026 16:54:57 +0300 +Subject: [PATCH 1/4] 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) +--- + .../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 0000000..59ced18 +--- /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 0000000..8d98a04 +--- /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 0000000..6efcd95 +--- /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 0000000..e81cee7 +--- /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 0000000..edcc234 +--- /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 0000000..618f9d3 +--- /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 0000000..17d3e18 +--- /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 0000000..039695d +--- /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 0000000..c41bb7e +--- /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 + +From a01a313653ad35eccf30e0001dfcdca6b66884fa Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ayd=C4=B1n=20Mercan?= +Date: Thu, 7 May 2026 18:59:20 +0300 +Subject: [PATCH 2/4] 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) +--- + lib/dns/dnssec.c | 13 +++++++++++++ + lib/dns/include/dns/dnssec.h | 6 ++++++ + 2 files changed, 19 insertions(+) + +diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c +index 86f0368..9f2105e 100644 +--- a/lib/dns/dnssec.c ++++ b/lib/dns/dnssec.c +@@ -376,8 +376,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; +@@ -469,6 +471,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; ++ } ++ } + + /* + * Is the key allowed to sign data? +diff --git a/lib/dns/include/dns/dnssec.h b/lib/dns/include/dns/dnssec.h +index cb8fd9d..2be11b9 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_* + */ + +-- +2.55.0 + +From 6d2f2273b904471f8e1713ef6946e6ce6abad577 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ayd=C4=B1n=20Mercan?= +Date: Tue, 12 May 2026 14:54:09 +0300 +Subject: [PATCH 3/4] 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) +--- + lib/dns/include/dns/nsec.h | 18 ++++++++++---- + lib/dns/nsec.c | 17 ++++++++++---- + lib/dns/resolver.c | 48 ++++++++++++++++++++++++++++++++++++-- + lib/ns/query.c | 6 ++--- + 4 files changed, 75 insertions(+), 14 deletions(-) + +diff --git a/lib/dns/include/dns/nsec.h b/lib/dns/include/dns/nsec.h +index e68ea35..047e3c4 100644 +--- a/lib/dns/include/dns/nsec.h ++++ b/lib/dns/include/dns/nsec.h +@@ -114,13 +114,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 fe53e2a..9fb79f8 100644 +--- a/lib/dns/nsec.c ++++ b/lib/dns/nsec.c +@@ -21,6 +21,7 @@ + #include + + #include ++#include + #include + #include + #include +@@ -504,8 +505,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; + +@@ -520,12 +522,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 a4efc9a..40480c5 100644 +--- a/lib/dns/resolver.c ++++ b/lib/dns/resolver.c +@@ -63,7 +63,9 @@ + #include + #include + #include ++#include + #include ++#include + #include + + /* Detailed logging of fctx attach/detach */ +@@ -5540,6 +5542,36 @@ has_000_label(dns_rdataset_t *nsecset) { + return false; + } + ++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. + */ +@@ -5570,6 +5602,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 */ + +@@ -5959,10 +5993,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 0f61fe1..2ef4111 100644 +--- a/lib/ns/query.c ++++ b/lib/ns/query.c +@@ -10356,10 +10356,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 + +From 88d9e228ca7496bb1f971163249eeae848f35ee7 Mon Sep 17 00:00:00 2001 +From: RHEL Packaging Agent +Date: Thu, 23 Jul 2026 13:15:20 +0000 +Subject: [PATCH 4/4] Fix RETERR usage in dns_dnssec_verify - use ret variable + instead of result + +The dns_dnssec_verify function uses 'ret' as its local variable name and +has no 'failure:' label, but the RETERR macro assumes 'result' and +'goto failure'. Replace RETERR() calls with direct error checking using +the 'ret' variable and immediate returns. +--- + lib/dns/dnssec.c | 10 ++++++++-- + 1 file changed, 8 insertions(+), 2 deletions(-) + +diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c +index 9f2105e..546df26 100644 +--- a/lib/dns/dnssec.c ++++ b/lib/dns/dnssec.c +@@ -475,9 +475,15 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key, + * Check for out of zone NSEC entries. + */ + if (set->type == dns_rdatatype_nsec) { +- RETERR(dns_rdataset_first(set)); ++ ret = dns_rdataset_first(set); ++ if (ret != ISC_R_SUCCESS) { ++ return ret; ++ } + dns_rdataset_current(set, &rdata); +- RETERR(dns_rdata_tostruct(&rdata, &nsec, NULL)); ++ ret = dns_rdata_tostruct(&rdata, &nsec, NULL); ++ if (ret != ISC_R_SUCCESS) { ++ return ret; ++ } + if (!dns_name_issubdomain(&nsec.next, &sig.signer)) { + return DNS_R_NOVALIDNSEC; + } +-- +2.55.0 + diff --git a/bind.spec b/bind.spec index 5a151dd..4f74e96 100644 --- a/bind.spec +++ b/bind.spec @@ -80,7 +80,7 @@ License: MPL-2.0 AND ISC AND MIT AND BSD-3-Clause AND BSD-2-Clause # Before rebasing bind, ensure bind-dyndb-ldap is ready to be rebuild and use side-tag with it. # Updating just bind will cause freeipa-dns-server package to be uninstallable. Version: 9.18.33 -Release: 23%{?dist} +Release: 24%{?dist} Epoch: 32 Url: https://www.isc.org/downloads/bind/ # @@ -178,6 +178,8 @@ Patch237: bind-9.18-CVE-2026-11331.patch Patch238: bind-9.18-CVE-2026-11721.patch # https://github.com/isc-projects/bind9/commit/231b1ca3edfb26389e1af39181aa6b4413e87ec4 Patch239: bind-9.18-CVE-2026-11622.patch +# https://github.com/isc-projects/bind9/commit/36f3d50f9c8ebc8d25ee033e707ca502e20b083f +Patch240: bind-9.18-CVE-2026-13321.patch %{?systemd_ordering} # https://fedoraproject.org/wiki/Changes/RPMSuportForSystemdSysusers @@ -979,6 +981,9 @@ fi; %endif %changelog +* Fri Jul 24 2026 RHEL Packaging Agent - 32:9.18.33-24 +- Reject out-of-zone NSEC next owner names (CVE-2026-13321) + * Thu Jul 23 2026 RHEL Packaging Agent - 32:9.18.33-23 - Fix reference-counted dns_slabheaders in cache (CVE-2026-11622)