Fix CVE-2026-13321: reject out-of-zone NSEC entries in DNSSEC validation

Add patch for CVE-2026-13321 which rejects out-of-zone NSEC
next owner names during DNSSEC validation. The patch is based
on ISC's 9.11-specific backport commits (3bc7280158 and
27ceccfe77) which add a dns_nsec_is_legal() function and
integrate NSEC validation checks into the resolver, using
9.11-compatible APIs and coding conventions.

CVE: CVE-2026-13321
Upstream patches:
 - 058023c66f.patch
 - f751e19a30.patch
Resolves: RHEL-213318

This commit was backported by Ymir, a Red Hat Enterprise Linux software maintenance AI agent.

Assisted-by: Ymir
This commit is contained in:
RHEL Packaging Agent 2026-07-27 15:22:12 +00:00
parent 3b30e917e8
commit 0f28e7dd60
2 changed files with 279 additions and 1 deletions

View File

@ -0,0 +1,270 @@
From 338661714d508a810a903ae6fdaf522c9abc2d4b 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 1/2] 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 | 18 ++++++++++++++++++
lib/dns/include/dns/dnssec.h | 6 ++++++
2 files changed, 24 insertions(+)
diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c
index b6b2405fac..b9bd374ed6 100644
--- a/lib/dns/dnssec.c
+++ b/lib/dns/dnssec.c
@@ -380,8 +380,10 @@ dns_dnssec_verify3(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;
@@ -454,6 +456,22 @@ dns_dnssec_verify3(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) {
+ if (dns_rdataset_first(set) != ISC_R_SUCCESS) {
+ return (DNS_R_NOVALIDNSEC);
+ }
+ dns_rdataset_current(set, &rdata);
+ if (dns_rdata_tostruct(&rdata, &nsec, NULL) != ISC_R_SUCCESS) {
+ return (DNS_R_NOVALIDNSEC);
+ }
+ 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 31e2586c9b..1f13a08181 100644
--- a/lib/dns/include/dns/dnssec.h
+++ b/lib/dns/include/dns/dnssec.h
@@ -138,6 +138,9 @@ dns_dnssec_verify3(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:
@@ -160,6 +163,9 @@ dns_dnssec_verify3(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_*
*/
From dfb91ca4a45092c414a41cb5640cf4710f7ba0e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ayd=C4=B1n=20Mercan?= <aydin@isc.org>
Date: Tue, 12 May 2026 14:54:09 +0300
Subject: [PATCH 2/2] 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 | 37 +++++++++++++++++++++++++++
lib/dns/resolver.c | 51 ++++++++++++++++++++++++++++++++++++++
3 files changed, 106 insertions(+)
diff --git a/lib/dns/include/dns/nsec.h b/lib/dns/include/dns/nsec.h
index 4e12cbea75..8caead6a1e 100644
--- a/lib/dns/include/dns/nsec.h
+++ b/lib/dns/include/dns/nsec.h
@@ -106,6 +106,24 @@ dns_nsec_noexistnodata(dns_rdatatype_t type, dns_name_t *name,
* Return ISC_R_IGNORE when the NSEC is not the appropriate one.
*/
+bool
+dns_nsec_is_legal(dns_rdataset_t *rdataset, const dns_name_t *name);
+/**<
+ * \brief
+ * Validates a rdataset of type NSEC.
+ *
+ * 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
#endif /* DNS_NSEC_H */
diff --git a/lib/dns/nsec.c b/lib/dns/nsec.c
index d90c38589e..fd4c529da0 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>
@@ -445,3 +446,39 @@ dns_nsec_noexistnodata(dns_rdatatype_t type, dns_name_t *name,
*exists = false;
return (ISC_R_SUCCESS);
}
+
+bool
+dns_nsec_is_legal(dns_rdataset_t *nsecset, const dns_name_t *name) {
+ dns_rdataset_t rdataset;
+ dns_rdata_nsec_t nsec;
+ isc_result_t result;
+ bool found = false;
+
+ REQUIRE(nsecset != NULL && nsecset->type == dns_rdatatype_nsec);
+
+ dns_rdataset_init(&rdataset);
+ dns_rdataset_clone(nsecset, &rdataset);
+
+ for (result = dns_rdataset_first(&rdataset); result == ISC_R_SUCCESS;
+ result = dns_rdataset_next(&rdataset))
+ {
+ dns_rdata_t rdata = DNS_RDATA_INIT;
+ dns_rdataset_current(&rdataset, &rdata);
+
+ /* 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);
+ return (found);
+}
diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c
index 0f16eadcdd..8ef35bb641 100644
--- a/lib/dns/resolver.c
+++ b/lib/dns/resolver.c
@@ -62,8 +62,10 @@
#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>
#ifdef WANT_QUERYTRACE
#define RTRACE(m) isc_log_write(dns_lctx, \
@@ -4879,6 +4881,36 @@ maybe_destroy(fetchctx_t *fctx, bool locked) {
return (bucket_empty);
}
+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, NULL);
+
+ 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.
*/
@@ -4907,6 +4939,8 @@ validated(isc_task_t *task, isc_event_t *event) {
unsigned options;
uint32_t bucketnum;
dns_message_t *rmessage = NULL;
+ dns_fixedname_t fsigner;
+ dns_name_t *signer = NULL;
UNUSED(task); /* for now */
@@ -5240,6 +5274,23 @@ validated(isc_task_t *task, isc_event_t *event) {
if (sigrdataset == NULL ||
sigrdataset->trust != dns_trust_secure)
continue;
+ /*
+ * 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_is_legal(rdataset, signer))
+ {
+ continue;
+ }
result = dns_db_findnode(fctx->cache, name, true,
&nsnode);
if (result != ISC_R_SUCCESS)

View File

@ -68,7 +68,7 @@ Summary: The Berkeley Internet Name Domain (BIND) DNS (Domain Name System) serv
Name: bind
License: MPLv2.0
Version: 9.11.36
Release: 16%{?PATCHVER:.%{PATCHVER}}%{?PREVER:.%{PREVER}}%{?dist}.10
Release: 16%{?PATCHVER:.%{PATCHVER}}%{?PREVER:.%{PREVER}}%{?dist}.11
Epoch: 32
Url: https://www.isc.org/downloads/bind/
#
@ -217,6 +217,9 @@ Patch218: bind-9.11-CVE-2026-11622.patch
# https://github.com/isc-projects/bind9/commit/06778424f0f58375f0f3d6632813558c59d705c6
# https://github.com/isc-projects/bind9/commit/e5b16cfd1c0b7d7760108e1ca9e1fd9db30d9e6a
Patch219: bind-9.11-CVE-2026-11721.patch
# https://github.com/isc-projects/bind9/commit/058023c66f11d78590d4aa8c4f98946c4c965e21
# https://github.com/isc-projects/bind9/commit/f751e19a30d107f04c2f644aff9f8dab8fed03ab
Patch220: bind-9.11-CVE-2026-13321.patch
# SDB patches
Patch11: bind-9.3.2b2-sdbsrc.patch
@ -651,6 +654,7 @@ are used for building ISC DHCP.
%patch -P 217 -p1 -b .CVE-2026-5946
%patch -P 218 -p1 -b .CVE-2026-11622
%patch -P 219 -p1 -b .CVE-2026-11721
%patch -P 220 -p1 -b .CVE-2026-13321
mkdir lib/dns/tests/testdata/dstrandom
cp -a %{SOURCE50} lib/dns/tests/testdata/dstrandom/random.data
@ -1703,6 +1707,10 @@ rm -rf ${RPM_BUILD_ROOT}
%endif
%changelog
* Mon Jul 27 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 32:9.11.36-16.11
- Reject out-of-zone NSEC entries in DNSSEC validation
(CVE-2026-13321)
* Mon Jul 27 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 32:9.11.36-16.10
- Reject RRSIG records with invalid label counts (CVE-2026-11721)