From 368c75a9f567f8b36cf24fefe45023e0a050e47b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Sat, 14 Feb 2026 14:43:41 +0100 Subject: [PATCH 1/4] Invalid NSEC3 can cause OOB read of the isdelegation() stack When .next_length is longer than NSEC3_MAX_HASH_LENGTH, it causes a harmless out-of-bound read of the isdelegation() stack. This patch fixes the issue by skipping NSEC3 records with an oversized hash length during validation. (cherry picked from commit 67b4fb56e40bf856e1fccd41e752d5f486b5b569) --- lib/dns/rdata/generic/nsec3_50.c | 1 + lib/dns/validator.c | 3 +++ 2 files changed, 4 insertions(+) diff --git a/lib/dns/rdata/generic/nsec3_50.c b/lib/dns/rdata/generic/nsec3_50.c index f45fe4dc33..e04587bd1b 100644 --- a/lib/dns/rdata/generic/nsec3_50.c +++ b/lib/dns/rdata/generic/nsec3_50.c @@ -324,6 +324,7 @@ tostruct_nsec3(ARGS_TOSTRUCT) { } nsec3->mctx = mctx; + return ISC_R_SUCCESS; cleanup: diff --git a/lib/dns/validator.c b/lib/dns/validator.c index 809b7be911..9ec13581ab 100644 --- a/lib/dns/validator.c +++ b/lib/dns/validator.c @@ -339,6 +339,9 @@ trynsec3: if (nsec3.hash != 1) { continue; } + if (nsec3.next_length > NSEC3_MAX_HASH_LENGTH) { + continue; + } length = isc_iterated_hash( hash, nsec3.hash, nsec3.iterations, nsec3.salt, nsec3.salt_length, name->ndata, name->length); From 8d6e1c1a4842208669531aa50c50a13bc70b7802 Mon Sep 17 00:00:00 2001 From: Mark Andrews Date: Wed, 18 Feb 2026 12:30:22 +1100 Subject: [PATCH 2/4] Enforce NSEC3 record consistency NSEC3 hashes are required to fit within a single DNS label. Since there are 5 bits per label byte without pad characters, the maximum hash size is floor(63*5/8) (39 bytes). This patch enforces this maximum length for unknown algorithms, while strictly enforcing the exact expected digest length for known algorithms like SHA-1. (cherry picked from commit 3801d0ebbf8da69077af84dae7f7ec23718b839b) --- bin/tests/system/checkzone/zones/crashzone.db | 1 - lib/dns/include/dns/nsec3.h | 6 ++++ lib/dns/rdata/generic/nsec3_50.c | 33 ++++++++++++++++--- lib/isc/include/isc/iterated_hash.h | 12 ------- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/bin/tests/system/checkzone/zones/crashzone.db b/bin/tests/system/checkzone/zones/crashzone.db index 2a62e2a09d..169cbe331e 100644 --- a/bin/tests/system/checkzone/zones/crashzone.db +++ b/bin/tests/system/checkzone/zones/crashzone.db @@ -47,7 +47,6 @@ FQ7RBG86KRMACA1NAAKP2KQRQALBA0C7.dyn.example.net. 7200 RRSIG NSEC3 7 4 7200 201 577WZnTQemStx+diON9rEGXAGnU7C0KLjrFL VyhocnBnNtxJS8eRMSWvb9XuYCMNhYKOurtt Ar4qh4VW1+unmA== ) -I7A7A184GGMI35K1E3IR650LKO7NOB5R.dyn.example.net. 7200 IN NSEC3 1 0 10 76931F IMQ912BREQP1POLAH3RMONG;UED541AS A RRSIG IMQ912BREQP1POLAH3RMONG3UED541AS.dyn.example.net. 7200 IN NSEC3 1 0 10 76931F S3USV4M1HLVJ8F88EDSG8N9PVQRQ20N7 A RRSIG 7200 RRSIG NSEC3 7 4 7200 20100227180048 ( 20100221180048 30323 dyn.example.net. diff --git a/lib/dns/include/dns/nsec3.h b/lib/dns/include/dns/nsec3.h index e4da790b06..4a2c1e1a61 100644 --- a/lib/dns/include/dns/nsec3.h +++ b/lib/dns/include/dns/nsec3.h @@ -28,6 +28,12 @@ #define DNS_NSEC3_SALTSIZE 255 #define DNS_NSEC3_MAXITERATIONS 150U +/* + * The maximum hash that can be encoded in a single label using + * base32hexnp. floor(63*5/8) + */ +#define NSEC3_MAX_HASH_LENGTH 39 + /* * hash = 1, flags =1, iterations = 2, salt length = 1, salt = 255 (max) * hash length = 1, hash = 255 (max), bitmap = 8192 + 512 (max) diff --git a/lib/dns/rdata/generic/nsec3_50.c b/lib/dns/rdata/generic/nsec3_50.c index e04587bd1b..b42ab29a5d 100644 --- a/lib/dns/rdata/generic/nsec3_50.c +++ b/lib/dns/rdata/generic/nsec3_50.c @@ -35,6 +35,8 @@ #include #include +#include + #define RRTYPE_NSEC3_ATTRIBUTES DNS_RDATATYPEATTR_DNSSEC static isc_result_t @@ -96,8 +98,17 @@ fromtext_nsec3(ARGS_FROMTEXT) { false)); isc_buffer_init(&b, buf, sizeof(buf)); RETTOK(isc_base32hexnp_decodestring(DNS_AS_STR(token), &b)); - if (isc_buffer_usedlength(&b) > 0xffU) { - RETTOK(ISC_R_RANGE); + switch (hashalg) { + case dns_hash_sha1: + if (isc_buffer_usedlength(&b) != ISC_SHA1_DIGESTLENGTH) { + RETTOK(ISC_R_RANGE); + } + break; + default: + if (isc_buffer_usedlength(&b) > NSEC3_MAX_HASH_LENGTH) { + RETTOK(ISC_R_RANGE); + } + break; } RETERR(uint8_tobuffer(isc_buffer_usedlength(&b), target)); RETERR(mem_tobuffer(target, &buf, isc_buffer_usedlength(&b))); @@ -184,7 +195,7 @@ totext_nsec3(ARGS_TOTEXT) { static isc_result_t fromwire_nsec3(ARGS_FROMWIRE) { isc_region_t sr, rr; - unsigned int saltlen, hashlen; + unsigned int hash, saltlen, hashlen; REQUIRE(type == dns_rdatatype_nsec3); @@ -200,6 +211,7 @@ fromwire_nsec3(ARGS_FROMWIRE) { if (sr.length < 5U) { RETERR(DNS_R_FORMERR); } + hash = sr.base[0]; saltlen = sr.base[4]; isc_region_consume(&sr, 5); @@ -214,8 +226,19 @@ fromwire_nsec3(ARGS_FROMWIRE) { hashlen = sr.base[0]; isc_region_consume(&sr, 1); - if (hashlen < 1 || sr.length < hashlen) { - RETERR(DNS_R_FORMERR); + switch (hash) { + case dns_hash_sha1: + if (hashlen != ISC_SHA1_DIGESTLENGTH || sr.length < hashlen) { + RETERR(DNS_R_FORMERR); + } + break; + default: + if (hashlen < 1 || hashlen > NSEC3_MAX_HASH_LENGTH || + sr.length < hashlen) + { + RETERR(DNS_R_FORMERR); + } + break; } isc_region_consume(&sr, hashlen); diff --git a/lib/isc/include/isc/iterated_hash.h b/lib/isc/include/isc/iterated_hash.h index b5d6ab676b..ea96b335e1 100644 --- a/lib/isc/include/isc/iterated_hash.h +++ b/lib/isc/include/isc/iterated_hash.h @@ -15,18 +15,6 @@ #include -/* - * The maximal hash length that can be encoded in a name - * using base32hex. floor(255/8)*5 - */ -#define NSEC3_MAX_HASH_LENGTH 155 - -/* - * The maximum has that can be encoded in a single label using - * base32hex. floor(63/8)*5 - */ -#define NSEC3_MAX_LABEL_HASH 35 - ISC_LANG_BEGINDECLS int From 7eeefdc36a0db14d8b425a047390f68322ac2327 Mon Sep 17 00:00:00 2001 From: Mark Andrews Date: Tue, 24 Feb 2026 13:30:43 +1100 Subject: [PATCH 3/4] Remove invalid REQUIRE in NSEC3 fromstruct method The NSEC3 fromstruct method only worked for hash type 1 when it should work for all hash types. (cherry picked from commit f030bc6756c3d4b01fe587205059149d05a4e3b6) --- lib/dns/rdata/generic/nsec3_50.c | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/dns/rdata/generic/nsec3_50.c b/lib/dns/rdata/generic/nsec3_50.c index b42ab29a5d..413a82ef3d 100644 --- a/lib/dns/rdata/generic/nsec3_50.c +++ b/lib/dns/rdata/generic/nsec3_50.c @@ -288,7 +288,6 @@ fromstruct_nsec3(ARGS_FROMSTRUCT) { REQUIRE(nsec3->common.rdtype == type); REQUIRE(nsec3->common.rdclass == rdclass); REQUIRE(nsec3->typebits != NULL || nsec3->len == 0); - REQUIRE(nsec3->hash == dns_hash_sha1); UNUSED(type); UNUSED(rdclass); From e9c23f598bfcb90e6a39eb418d8f30391e42b13b Mon Sep 17 00:00:00 2001 From: Mark Andrews Date: Tue, 24 Feb 2026 13:35:07 +1100 Subject: [PATCH 4/4] Test maximum length NSEC3 hash detection Adds text and wire format unit tests to verify the newly enforced maximum NSEC3 hash length constraints. These tests ensure that hash lengths up to the 39-byte maximum are accepted, while larger sizes correctly fail. (cherry picked from commit e83a182056b5624566a576669417e62eb94bffe9) --- tests/dns/rdata_test.c | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/dns/rdata_test.c b/tests/dns/rdata_test.c index a43d28abe7..6354819d10 100644 --- a/tests/dns/rdata_test.c +++ b/tests/dns/rdata_test.c @@ -2374,8 +2374,7 @@ ISC_RUN_TEST_IMPL(nsec) { * RFC 5155. */ ISC_RUN_TEST_IMPL(nsec3) { - text_ok_t text_ok[] = { TEXT_INVALID(""), - TEXT_INVALID("."), + text_ok_t text_ok[] = { TEXT_INVALID(""), TEXT_INVALID("."), TEXT_INVALID(". RRSIG"), TEXT_INVALID("1 0 10 76931F"), TEXT_INVALID("1 0 10 76931F " @@ -2391,9 +2390,38 @@ ISC_RUN_TEST_IMPL(nsec3) { "AJHVGTICN6K0VDA53GCHFMT219SRRQLM"), TEXT_VALID("1 0 10 - " "AJHVGTICN6K0VDA53GCHFMT219SRRQLM"), + /* 123456789012345678901234567890123456789 */ + TEXT_VALID("2 0 10 - " + "64P36D1L6ORJGE9G64P36D1L6ORJGE9G64P" + "36D1L6ORJGE9G64P36D1L6ORJGE8"), + /* 1234567890123456789012345678901234567890 */ + TEXT_INVALID("2 0 10 - " + "64P36D1L6ORJGE9G64P36D1L6ORJGE9G6" + "4P36D1L6ORJGE9G64P36D1L6ORJGE9G"), TEXT_SENTINEL() }; + wire_ok_t wire_ok[] = { + WIRE_VALID(0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00), + /* maximal hash */ + WIRE_VALID(0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x01, 0x02, 0x03, + 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09), + /* Too big hash */ + WIRE_INVALID(0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09, 0x00), + /* + * Sentinel. + */ + WIRE_SENTINEL() + }; - check_rdata(text_ok, NULL, NULL, false, dns_rdataclass_in, + check_rdata(text_ok, wire_ok, NULL, false, dns_rdataclass_in, dns_rdatatype_nsec3, sizeof(dns_rdata_nsec3_t)); }