bind/bind-9.11-CVE-2026-11721.patch
RHEL Packaging Agent 3b30e917e8 Fix CVE-2026-11721: reject RRSIG records with invalid label counts
Backport fix for CVE-2026-11721 to bind 9.11 on c8s. The patch
combines two upstream commits (06778424f0 and e5b16cfd1c) that
prevent dnssec-signzone from signing out-of-zone records and
reject RRSIG records whose labels field indicates fewer labels
than the signer name requires, closing a cache poisoning vector
via forged wildcard records.

CVE: CVE-2026-11721
Upstream patches:
 - 06778424f0.patch
 - e5b16cfd1c.patch
Resolves: RHEL-213407

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

Assisted-by: Ymir
2026-07-27 15:36:48 +00:00

264 lines
8.4 KiB
Diff

From 78ac82303bc3dd9d426d21fb268a5ed362d5535a Mon Sep 17 00:00:00 2001
From: Mark Andrews <marka@isc.org>
Date: Tue, 14 Apr 2026 15:14:06 +1000
Subject: [PATCH 1/2] Don't sign out of zone records in dnssec-signzone
dnssec-signzone was signing extraneous records that were not within
the namespace of the zone. This no longer occurs.
(cherry picked from commit e45c9af7051421fd370f20ba8325199c606223fd)
---
bin/dnssec/dnssec-signzone.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/bin/dnssec/dnssec-signzone.c b/bin/dnssec/dnssec-signzone.c
index 6567421..b7c8e55 100644
--- a/bin/dnssec/dnssec-signzone.c
+++ b/bin/dnssec/dnssec-signzone.c
@@ -1482,6 +1482,11 @@ assignwork(isc_task_t *task, isc_task_t *worker) {
dns_db_detachnode(gdb, &node);
goto next;
}
+ if (!dns_name_issubdomain(name, gorigin)) {
+ dumpnode(name, node);
+ dns_db_detachnode(gdb, &node);
+ goto next;
+ }
/*
* Sort the zone data from the glue and out-of-zone data.
* For NSEC zones nodes with zone data have NSEC records.
From 8c269a0269fdf12d5edd4698d08d663c6f64ddae Mon Sep 17 00:00:00 2001
From: Mark Andrews <marka@isc.org>
Date: Tue, 14 Apr 2026 12:24:33 +1000
Subject: [PATCH 2/2] Invalid signed wildcard records were being accepted
An RRSIG whose Labels field indicates fewer labels than its signer
name requires was being accepted. When such a record covers a
wildcard, the validator reconstructs a wildcard owner name above the
signer's zone and caches it as secure. RFC 8198 cache synthesis
(synth-from-dnssec) then serves that forged wildcard for unrelated
names, poisoning the cache.
These records are now rejected, both when an RRSIG is parsed and when
its signature is verified.
(cherry picked from commit 084ca5ee10515e461d46b63df9660b8394bc7de9)
---
lib/dns/dnssec.c | 45 +++++++++++++++++++++++---------
lib/dns/rdata/generic/rrsig_46.c | 37 +++++++++++++++++++-------
2 files changed, 60 insertions(+), 22 deletions(-)
diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c
index b6b2405..2516a9a 100644
--- a/lib/dns/dnssec.c
+++ b/lib/dns/dnssec.c
@@ -142,11 +142,11 @@ dns_dnssec_keyfromrdata(dns_name_t *name, dns_rdata_t *rdata, isc_mem_t *mctx,
isc_buffer_t b;
isc_region_t r;
- INSIST(name != NULL);
- INSIST(rdata != NULL);
- INSIST(mctx != NULL);
- INSIST(key != NULL);
- INSIST(*key == NULL);
+ REQUIRE(name != NULL);
+ REQUIRE(rdata != NULL);
+ REQUIRE(mctx != NULL);
+ REQUIRE(key != NULL);
+ REQUIRE(*key == NULL);
REQUIRE(rdata->type == dns_rdatatype_key ||
rdata->type == dns_rdatatype_dnskey);
@@ -200,12 +200,14 @@ dns_dnssec_sign(dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
isc_buffer_t *databuf = NULL;
char data[256 + 8];
uint32_t flags;
+ unsigned int labels;
unsigned int sigsize;
dns_fixedname_t fnewname;
dns_fixedname_t fsigner;
REQUIRE(name != NULL);
- REQUIRE(dns_name_countlabels(name) <= 255);
+ labels = dns_name_countlabels(name);
+ REQUIRE(labels <= 255 && labels > 0);
REQUIRE(set != NULL);
REQUIRE(key != NULL);
REQUIRE(inception != NULL);
@@ -243,7 +245,7 @@ dns_dnssec_sign(dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
sig.covered = set->type;
sig.algorithm = dst_key_alg(key);
- sig.labels = dns_name_countlabels(name) - 1;
+ sig.labels = labels - 1;
if (dns_name_iswildcard(name))
sig.labels--;
sig.originalttl = set->ttl;
@@ -390,11 +392,14 @@ dns_dnssec_verify3(dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
isc_result_t ret;
unsigned char data[300];
dst_context_t *ctx = NULL;
- int labels = 0;
uint32_t flags;
+ unsigned int labels;
+ unsigned int siglabels;
bool downcase = false;
REQUIRE(name != NULL);
+ labels = dns_name_countlabels(name);
+ REQUIRE(labels > 0);
REQUIRE(set != NULL);
REQUIRE(key != NULL);
REQUIRE(mctx != NULL);
@@ -407,6 +412,21 @@ dns_dnssec_verify3(dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
if (set->type != sig.covered)
return (DNS_R_SIGINVALID);
+ /*
+ * The RRSIG labels field can't indicate fewer labels than the
+ * signer. Also the labels shouldn't be greater than that of
+ * the owner name.
+ *
+ * sig.labels doesn't include the root label, so add 1 to account
+ * for it.
+ */
+ siglabels = sig.labels + 1;
+ if (siglabels < dns_name_countlabels(&sig.signer) || siglabels > labels)
+ {
+ inc_stat(dns_dnssecstats_fail);
+ return DNS_R_SIGINVALID;
+ }
+
if (isc_serial_lt(sig.timeexpire, sig.timesigned)) {
inc_stat(dns_dnssecstats_fail);
return (DNS_R_SIGINVALID);
@@ -484,11 +504,10 @@ dns_dnssec_verify3(dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
* If the name is an expanded wildcard, use the wildcard name.
*/
dns_fixedname_init(&fnewname);
- labels = dns_name_countlabels(name) - 1;
RUNTIME_CHECK(dns_name_downcase(name, dns_fixedname_name(&fnewname),
NULL) == ISC_R_SUCCESS);
- if (labels - sig.labels > 0)
- dns_name_split(dns_fixedname_name(&fnewname), sig.labels + 1,
+ if (labels > siglabels)
+ dns_name_split(dns_fixedname_name(&fnewname), siglabels,
NULL, dns_fixedname_name(&fnewname));
dns_name_toregion(dns_fixedname_name(&fnewname), &r);
@@ -497,7 +516,7 @@ dns_dnssec_verify3(dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
* Create an envelope for each rdata: <name|type|class|ttl>.
*/
isc_buffer_init(&envbuf, data, sizeof(data));
- if (labels - sig.labels > 0) {
+ if (labels > siglabels) {
isc_buffer_putuint8(&envbuf, 1);
isc_buffer_putuint8(&envbuf, '*');
memmove(data + 2, r.base, r.length);
@@ -583,7 +602,7 @@ cleanup_struct:
if (ret != ISC_R_SUCCESS)
inc_stat(dns_dnssecstats_fail);
- if (ret == ISC_R_SUCCESS && labels - sig.labels > 0) {
+ if (ret == ISC_R_SUCCESS && labels > siglabels) {
if (wild != NULL)
RUNTIME_CHECK(dns_name_concatenate(dns_wildcardname,
dns_fixedname_name(&fnewname),
diff --git a/lib/dns/rdata/generic/rrsig_46.c b/lib/dns/rdata/generic/rrsig_46.c
index 0d4df67..35a1b4c 100644
--- a/lib/dns/rdata/generic/rrsig_46.c
+++ b/lib/dns/rdata/generic/rrsig_46.c
@@ -21,12 +21,12 @@
static inline isc_result_t
fromtext_rrsig(ARGS_FROMTEXT) {
isc_token_t token;
- unsigned char c;
+ unsigned char alg, labels;
long i;
dns_rdatatype_t covered;
- char *e;
+ char *e = NULL;
isc_result_t result;
- dns_name_t name;
+ dns_name_t signer;
isc_buffer_t buffer;
uint32_t time_signed, time_expire;
@@ -57,8 +57,8 @@ fromtext_rrsig(ARGS_FROMTEXT) {
*/
RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_string,
false));
- RETTOK(dns_secalg_fromtext(&c, &token.value.as_textregion));
- RETERR(mem_tobuffer(target, &c, 1));
+ RETTOK(dns_secalg_fromtext(&alg, &token.value.as_textregion));
+ RETERR(mem_tobuffer(target, &alg, 1));
/*
* Labels.
@@ -67,8 +67,8 @@ fromtext_rrsig(ARGS_FROMTEXT) {
false));
if (token.value.as_ulong > 0xffU)
RETTOK(ISC_R_RANGE);
- c = (unsigned char)token.value.as_ulong;
- RETERR(mem_tobuffer(target, &c, 1));
+ labels = (unsigned char)token.value.as_ulong;
+ RETERR(mem_tobuffer(target, &labels, 1));
/*
* Original ttl.
@@ -131,11 +131,19 @@ fromtext_rrsig(ARGS_FROMTEXT) {
*/
RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_string,
false));
- dns_name_init(&name, NULL);
+ dns_name_init(&signer, NULL);
buffer_fromregion(&buffer, &token.value.as_region);
if (origin == NULL)
origin = dns_rootname;
- RETTOK(dns_name_fromtext(&name, &buffer, origin, options, target));
+ RETTOK(dns_name_fromtext(&signer, &buffer, origin, options, target));
+
+ /*
+ * (RRSIG labels doesn't include the root label, so add one
+ * to normalize it before checking against the signer.)
+ */
+ if ((unsigned int)(labels + 1) < dns_name_countlabels(&signer)) {
+ RETTOK(ISC_R_RANGE);
+ }
/*
* Sig.
@@ -260,6 +268,7 @@ static inline isc_result_t
fromwire_rrsig(ARGS_FROMWIRE) {
isc_region_t sr;
dns_name_t name;
+ unsigned char labels;
REQUIRE(type == dns_rdatatype_rrsig);
@@ -281,6 +290,8 @@ fromwire_rrsig(ARGS_FROMWIRE) {
if (sr.length < 18)
return (ISC_R_UNEXPECTEDEND);
+ labels = sr.base[3];
+
isc_buffer_forward(source, 18);
RETERR(mem_tobuffer(target, sr.base, 18));
@@ -290,6 +301,14 @@ fromwire_rrsig(ARGS_FROMWIRE) {
dns_name_init(&name, NULL);
RETERR(dns_name_fromwire(&name, source, dctx, options, target));
+ /*
+ * (RRSIG labels doesn't include the root label, so add one
+ * to normalize it before checking against the signer.)
+ */
+ if ((unsigned int)(labels + 1) < dns_name_countlabels(&name)) {
+ RETERR(DNS_R_FORMERR);
+ }
+
/*
* Sig.
*/