Several vulnerabilities were found in NSD.We released 4.15.3 as a security release on Wednesday 14 October including the relevant fixes.
The overview of the vulnerabilities with a brief description is: CVE-2026-101337 - severity: HIGH RCE class stack overwrite from specially formatted base64 RDATA field CVE-2026-101338 - severity: HIGH RCE class stack overwrite from a crafted and formatted SVCB or HTTPS RR CVE-2026-101339 - severity: HIGH RCE class stack overwrite from a crafted and formatted HIP RR CVE-2026-101341 - severity: HIGH RCE class stack overwrite from specially formatted TXT-DATA RDATA fieldYou can find detailed information on each vulnerability attached to this email along with their respective patches.
For ease of deployment we also provide a combined patch including all of them (patch_combined-4.15.3.diff).
The patches are tested to apply/work on 4.15.2. Best regards, -- Willem, on behalf of the NSD team.
The CVE number for this vulnerability is CVE-2026-101337 = Summary When reading zone files, a resource record with a base64 RDATA field in a specially crafted presentation format, can cause an out of bounds stack overwrite which can be exploited for execution of arbitrary command. == Affected products NSD from and including version 4.10.0 up to and including version 4.15.2 == Description When base64 data is split up in pieces, of 3 character or less, tokens were incorrectly calculated to produce 0 bytes. This enables attackers to overwrite the stack with up to 64K of attacker controlled data. Zone files which are written out by NSD itself, such as for zones for which the NSD instance is secondary, will not be formatted like this. The zone file must be fed to NSD directly, (but it could come, for example, from an operator controlled provisioning system). == Mitigation === Downloading patched version NSD 4.15.3 is released with the patch https://nlnetlabs.nl/downloads/nsd/nsd-4.15.3.tar.gz === Applying the patch manually For NSD 4.15.2 the patch is: https://nlnetlabs.nl/downloads/nsd/patch_CVE-2026-101337.diff Apply the patch on the NSD source directory with: patch -p1 < patch_CVE-2026-101337.diff then run 'make install' to install NSD. The patch is tested to work on NSD 4.15.2. == Acknowledgments We would like to thank Qifan Zhang from Palo Alto Networks, and Gaurang Maheta for discovering and responsibly disclosing the vulnerability.
diff --git a/simdzone/src/generic/base64.h b/simdzone/src/generic/base64.h
index ac9ef29..176a5c9 100644
--- a/simdzone/src/generic/base64.h
+++ b/simdzone/src/generic/base64.h
@@ -701,9 +701,12 @@ static really_inline int32_t parse_base64_sequence(
struct base64_state state = { .eof = 0, .bytes = 0, .carry = 0 };
do {
- size_t length = token->length / 4;
- if (((uintptr_t)rdata->limit - (uintptr_t)rdata->octets) / 3 < length)
+ /* reserve the maximum any token can emit, regardless of entry phase */
+ const size_t reserve = (token->length + 3) / 4 * 3;
+ if (rdata->octets > rdata->limit ||
+ (size_t)(rdata->limit - rdata->octets) < reserve)
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(item), NAME(type));
+ size_t length;
if (!base64_stream_decode(&state, token->data, token->length, rdata->octets, &length))
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(item), NAME(type));
rdata->octets += length;
@@ -726,9 +729,12 @@ static really_inline int32_t parse_base64(
rdata_t *rdata,
const token_t *token)
{
- size_t length = token->length / 4;
- if (((uintptr_t)rdata->limit - (uintptr_t)rdata->octets) / 3 < length)
+ /* reserve the maximum any token can emit, regardless of entry phase */
+ const size_t reserve = (token->length + 3) / 4 * 3;
+ if (rdata->octets > rdata->limit ||
+ (size_t)(rdata->limit - rdata->octets) < reserve)
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(item), NAME(type));
+ size_t length;
if (!base64_decode(token->data, token->length, rdata->octets, &length))
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(item), NAME(type));
rdata->octets += length;
--
2.43.0
The CVE number for this vulnerability is CVE-2026-101338 = Summary When reading zone files, a specially crafted and format SVCB or HTTPS record cancause an out of bounds stack overwrite which can be exploited for execution of arbitrary commands. == Affected products NSD from and including version 4.10.0 up to and including version 4.15.2 == Description When a SVCB or HTTPS RR has filled the wire format RDATA up to the brim, another ECH parameter may overwrite the stack, and once beyond the limit, the base64 decoder can write attacker controlled data of unlimited size. Zone files which are written out by NSD itself, such as for zones for which the NSD instance is secondary, will not be formatted like this. The zone file must be fed to NSD directly, (but it could come, for example, from an operator controlled provisioning system). == Mitigation === Downloading patched version NSD 4.15.3 is released with the patch https://nlnetlabs.nl/downloads/nsd/nsd-4.15.3.tar.gz === Applying the patch manually For NSD 4.15.2 the patch is: https://nlnetlabs.nl/downloads/nsd/patch_CVE-2026-101338.diff Apply the patch on the NSD source directory with: patch -p1 < patch_CVE-2026-101338.diff then run 'make install' to install NSD. The patch is tested to work on NSD 4.15.2. == Acknowledgments We would like to thank Qifan Zhang from Palo Alto Networks, for discovering and responsibly disclosing the vulnerability.
diff --git a/simdzone/src/generic/svcb.h b/simdzone/src/generic/svcb.h
index ffc932a..997ce0f 100644
--- a/simdzone/src/generic/svcb.h
+++ b/simdzone/src/generic/svcb.h
@@ -1075,6 +1075,8 @@ static really_inline int32_t parse_svc_params(
if (likely(key > highest_key)) {
highest_key = key;
octets = rdata->octets;
+ if (rdata->limit - rdata->octets < 4)
+ SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(field), NAME(type));
rdata->octets += 4;
if ((code = parse_svc_param(
parser, type, field, key, param, param->parse, rdata, value)))
--
2.43.0
The CVE number for this vulnerability is CVE-2026-101339 = Summary When reading zone files, a specially crafted and formatted HIP resource record can cause an out of bounds stack overwrite which can be exploited for execution of arbitrary commands. == Affected products NSD from and including version 4.10.0 up to and including version 4.15.2 == Description A HIP resource record that fills the RDATA near or at is limit, and has another dname RDATA field (a HIP Rendezvous server) can overwrite the stack with up to 191 bytes of attacker controlled data. Zone files which are written out by NSD itself, such as for zones for which the NSD instance is secondary, will not be formatted like this. The zone file must be fed to NSD directly, (but it could come, for example, from an operator controlled provisioning system). == Mitigation === Downloading patched version NSD 4.15.3 is released with the patch https://nlnetlabs.nl/downloads/nsd/nsd-4.15.3.tar.gz === Applying the patch manually For NSD 4.15.2 the patch is: https://nlnetlabs.nl/downloads/nsd/patch_CVE-2026-101339.diff Apply the patch on the NSD source directory with: patch -p1 < patch_CVE-2026-101339.diff then run 'make install' to install NSD. The patch is tested to work on NSD 4.15.2. == Acknowledgments We would like to thank Qifan Zhang from Palo Alto Networks, and Alwaleed Ashi (LXR) <[email protected]> for discovering and responsibly disclosing the vulnerability.
diff --git a/simdzone/src/fallback/name.h b/simdzone/src/fallback/name.h
index 2e67855..9ea9a96 100644
--- a/simdzone/src/fallback/name.h
+++ b/simdzone/src/fallback/name.h
@@ -9,15 +9,17 @@
#ifndef NAME_H
#define NAME_H
-nonnull_all
+nonnull((1,3,5))
static really_inline int32_t scan_name(
const char *data,
size_t length,
uint8_t octets[255 + ZONE_BLOCK_SIZE],
+ const uint8_t *limit,
size_t *lengthp)
{
uint8_t *l = octets, *w = octets + 1;
- const uint8_t *we = octets + 255;
+ const uint8_t *we = limit && limit < octets + 255
+ ? limit : octets + 255;
const char *t = data, *te = t + length;
l[0] = 0;
diff --git a/simdzone/src/generic/format.h b/simdzone/src/generic/format.h
index 76542c2..d167a27 100644
--- a/simdzone/src/generic/format.h
+++ b/simdzone/src/generic/format.h
@@ -52,8 +52,11 @@ static really_inline int32_t parse_name(
// a freestanding "@" denotes the current origin
if (unlikely(token->length == 1 && token->data[0] == '@'))
goto relative;
- switch (scan_name(token->data, token->length, rdata->octets, &length)) {
+ switch (scan_name(token->data, token->length, rdata->octets, rdata->limit, &length)) {
case 0:
+ if (rdata->octets + length > rdata->limit)
+ SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(field), NAME(type));
+
rdata->octets += length;
return 0;
case 1:
@@ -63,7 +66,8 @@ static really_inline int32_t parse_name(
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(field), NAME(type));
relative:
- if (length > 255 - parser->file->origin.length)
+ if (length > 255 - parser->file->origin.length
+ || rdata->octets + length + parser->file->origin.length > rdata->limit)
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(field), NAME(type));
memcpy(rdata->octets + length, parser->file->origin.octets, parser->file->origin.length);
rdata->octets += length + parser->file->origin.length;
@@ -85,7 +89,7 @@ static really_inline int32_t parse_owner(
// a freestanding "@" denotes the origin
if (unlikely(token->length == 1 && token->data[0] == '@'))
goto relative;
- switch (scan_name(token->data, token->length, octets, &length)) {
+ switch (scan_name(token->data, token->length, octets, NULL, &length)) {
case 0:
parser->file->owner.length = length;
parser->owner = &parser->file->owner;
@@ -263,7 +267,7 @@ static really_inline int32_t parse_dollar_include(
// $INCLUDE directive MAY specify an origin
take(parser, token);
if (is_contiguous(token)) {
- if (scan_name(token->data, token->length, name.octets, &name.length) != 0) {
+ if (scan_name(token->data, token->length, name.octets, NULL, &name.length) != 0) {
zone_close_file(parser, file);
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(&fields[1]), NAME(&include));
}
@@ -329,7 +333,7 @@ static inline int32_t parse_dollar_origin(
if ((code = take_contiguous_or_quoted(parser, &origin, &fields[0], token)) < 0)
return code;
- if (scan_name(token->data, token->length, parser->file->origin.octets, &parser->file->origin.length) != 0)
+ if (scan_name(token->data, token->length, parser->file->origin.octets, NULL, &parser->file->origin.length) != 0)
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(&fields[0]), NAME(&origin));
if ((code = take_delimiter(parser, &origin, token)) < 0)
return code;
diff --git a/simdzone/src/generic/name.h b/simdzone/src/generic/name.h
index d16c943..75486a6 100644
--- a/simdzone/src/generic/name.h
+++ b/simdzone/src/generic/name.h
@@ -26,18 +26,23 @@ static really_inline void copy_name_block(
block->dots = simd_find_8x32(&input, '.');
}
-nonnull_all
+nonnull((1,3,5))
static really_inline int32_t scan_name(
const char *data,
size_t tlength,
uint8_t octets[255 + ZONE_BLOCK_SIZE],
+ const uint8_t *limit,
size_t *lengthp)
{
uint64_t label = 0;
const char *text = data;
uint8_t *wire = octets + 1;
name_block_t block;
+ size_t max_length = limit && ((uintptr_t)limit - (uintptr_t)octets) < 255
+ ? ((uintptr_t)limit - (uintptr_t)octets) : 255;
+ if(max_length == 0)
+ return -1;
octets[0] = 0;
// real world domain names quickly exceed 16 octets (www.example.com is
@@ -146,9 +151,9 @@ escaped:
}
octets[label] = (uint8_t)(length - label);
- } while (left && length < 255);
+ } while (left && length < max_length);
- if (length >= 255)
+ if (length >= max_length)
return -1;
*lengthp = length + 1;
--
2.43.0
The CVE number for this vulnerability is CVE-2026-101341 = Summary When reading zone files, a specially crafted and formatted resource record containing a series of strings (TXT, NINFO, SPF, AVC, RESINFO and WALLET) can cause an out of bounds stack overwrite which can be exploited for executionof arbitrary commands. == Affected products NSD from and including version 4.10.0 up to and including version 4.15.2 == Description Tokens of text in RDATA (of for example TXT records) under 32 bytes are not checked to stay within the space reserved for the RDATA allowing for arbitrary sized out of bound stack writes, including an overwrite of a callback function pointer which is called directly after parse. Zone files which are written out by NSD itself, such for zones for which the NSD instance is secondary, will not be formatted like this. The zone file must be fed to NSD directly, (but it could come, for example, from an operator controlled provisioning system). == Mitigation === Downloading patched version NSD 4.15.3 is released with the patch https://nlnetlabs.nl/downloads/nsd/nsd-4.15.3.tar.gz === Applying the patch manually For NSD 4.15.2 the patch is: https://nlnetlabs.nl/downloads/nsd/patch_CVE-2026-101341.diff Apply the patch on the NSD source directory with: patch -p1 < patch_CVE-2026-101341.diff then run 'make install' to install NSD. The patch is tested to work on NSD 4.15.2. == Acknowledgments We would like to thank Alwaleed Ashi (LXR) <[email protected]>, for discovering and responsibly disclosing the vulnerability.
diff --git a/simdzone/src/generic/text.h b/simdzone/src/generic/text.h
index 2288c33..5d40062 100644
--- a/simdzone/src/generic/text.h
+++ b/simdzone/src/generic/text.h
@@ -66,7 +66,7 @@ static really_inline int32_t scan_string(
goto escaped;
if (length < 32)
- return (int32_t)count;
+ return octets + count <= limit ? (int32_t)count : -1;
text += count;
wire += count;
--
2.43.0
diff --git a/simdzone/src/generic/base64.h b/simdzone/src/generic/base64.h
index ac9ef29..176a5c9 100644
--- a/simdzone/src/generic/base64.h
+++ b/simdzone/src/generic/base64.h
@@ -701,9 +701,12 @@ static really_inline int32_t parse_base64_sequence(
struct base64_state state = { .eof = 0, .bytes = 0, .carry = 0 };
do {
- size_t length = token->length / 4;
- if (((uintptr_t)rdata->limit - (uintptr_t)rdata->octets) / 3 < length)
+ /* reserve the maximum any token can emit, regardless of entry phase */
+ const size_t reserve = (token->length + 3) / 4 * 3;
+ if (rdata->octets > rdata->limit ||
+ (size_t)(rdata->limit - rdata->octets) < reserve)
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(item), NAME(type));
+ size_t length;
if (!base64_stream_decode(&state, token->data, token->length, rdata->octets, &length))
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(item), NAME(type));
rdata->octets += length;
@@ -726,9 +729,12 @@ static really_inline int32_t parse_base64(
rdata_t *rdata,
const token_t *token)
{
- size_t length = token->length / 4;
- if (((uintptr_t)rdata->limit - (uintptr_t)rdata->octets) / 3 < length)
+ /* reserve the maximum any token can emit, regardless of entry phase */
+ const size_t reserve = (token->length + 3) / 4 * 3;
+ if (rdata->octets > rdata->limit ||
+ (size_t)(rdata->limit - rdata->octets) < reserve)
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(item), NAME(type));
+ size_t length;
if (!base64_decode(token->data, token->length, rdata->octets, &length))
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(item), NAME(type));
rdata->octets += length;
--
2.43.0
diff --git a/simdzone/src/generic/svcb.h b/simdzone/src/generic/svcb.h
index ffc932a..997ce0f 100644
--- a/simdzone/src/generic/svcb.h
+++ b/simdzone/src/generic/svcb.h
@@ -1075,6 +1075,8 @@ static really_inline int32_t parse_svc_params(
if (likely(key > highest_key)) {
highest_key = key;
octets = rdata->octets;
+ if (rdata->limit - rdata->octets < 4)
+ SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(field), NAME(type));
rdata->octets += 4;
if ((code = parse_svc_param(
parser, type, field, key, param, param->parse, rdata, value)))
--
2.43.0
diff --git a/simdzone/src/fallback/name.h b/simdzone/src/fallback/name.h
index 2e67855..9ea9a96 100644
--- a/simdzone/src/fallback/name.h
+++ b/simdzone/src/fallback/name.h
@@ -9,15 +9,17 @@
#ifndef NAME_H
#define NAME_H
-nonnull_all
+nonnull((1,3,5))
static really_inline int32_t scan_name(
const char *data,
size_t length,
uint8_t octets[255 + ZONE_BLOCK_SIZE],
+ const uint8_t *limit,
size_t *lengthp)
{
uint8_t *l = octets, *w = octets + 1;
- const uint8_t *we = octets + 255;
+ const uint8_t *we = limit && limit < octets + 255
+ ? limit : octets + 255;
const char *t = data, *te = t + length;
l[0] = 0;
diff --git a/simdzone/src/generic/format.h b/simdzone/src/generic/format.h
index 76542c2..d167a27 100644
--- a/simdzone/src/generic/format.h
+++ b/simdzone/src/generic/format.h
@@ -52,8 +52,11 @@ static really_inline int32_t parse_name(
// a freestanding "@" denotes the current origin
if (unlikely(token->length == 1 && token->data[0] == '@'))
goto relative;
- switch (scan_name(token->data, token->length, rdata->octets, &length)) {
+ switch (scan_name(token->data, token->length, rdata->octets, rdata->limit, &length)) {
case 0:
+ if (rdata->octets + length > rdata->limit)
+ SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(field), NAME(type));
+
rdata->octets += length;
return 0;
case 1:
@@ -63,7 +66,8 @@ static really_inline int32_t parse_name(
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(field), NAME(type));
relative:
- if (length > 255 - parser->file->origin.length)
+ if (length > 255 - parser->file->origin.length
+ || rdata->octets + length + parser->file->origin.length > rdata->limit)
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(field), NAME(type));
memcpy(rdata->octets + length, parser->file->origin.octets, parser->file->origin.length);
rdata->octets += length + parser->file->origin.length;
@@ -85,7 +89,7 @@ static really_inline int32_t parse_owner(
// a freestanding "@" denotes the origin
if (unlikely(token->length == 1 && token->data[0] == '@'))
goto relative;
- switch (scan_name(token->data, token->length, octets, &length)) {
+ switch (scan_name(token->data, token->length, octets, NULL, &length)) {
case 0:
parser->file->owner.length = length;
parser->owner = &parser->file->owner;
@@ -263,7 +267,7 @@ static really_inline int32_t parse_dollar_include(
// $INCLUDE directive MAY specify an origin
take(parser, token);
if (is_contiguous(token)) {
- if (scan_name(token->data, token->length, name.octets, &name.length) != 0) {
+ if (scan_name(token->data, token->length, name.octets, NULL, &name.length) != 0) {
zone_close_file(parser, file);
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(&fields[1]), NAME(&include));
}
@@ -329,7 +333,7 @@ static inline int32_t parse_dollar_origin(
if ((code = take_contiguous_or_quoted(parser, &origin, &fields[0], token)) < 0)
return code;
- if (scan_name(token->data, token->length, parser->file->origin.octets, &parser->file->origin.length) != 0)
+ if (scan_name(token->data, token->length, parser->file->origin.octets, NULL, &parser->file->origin.length) != 0)
SYNTAX_ERROR(parser, "Invalid %s in %s", NAME(&fields[0]), NAME(&origin));
if ((code = take_delimiter(parser, &origin, token)) < 0)
return code;
diff --git a/simdzone/src/generic/name.h b/simdzone/src/generic/name.h
index d16c943..75486a6 100644
--- a/simdzone/src/generic/name.h
+++ b/simdzone/src/generic/name.h
@@ -26,18 +26,23 @@ static really_inline void copy_name_block(
block->dots = simd_find_8x32(&input, '.');
}
-nonnull_all
+nonnull((1,3,5))
static really_inline int32_t scan_name(
const char *data,
size_t tlength,
uint8_t octets[255 + ZONE_BLOCK_SIZE],
+ const uint8_t *limit,
size_t *lengthp)
{
uint64_t label = 0;
const char *text = data;
uint8_t *wire = octets + 1;
name_block_t block;
+ size_t max_length = limit && ((uintptr_t)limit - (uintptr_t)octets) < 255
+ ? ((uintptr_t)limit - (uintptr_t)octets) : 255;
+ if(max_length == 0)
+ return -1;
octets[0] = 0;
// real world domain names quickly exceed 16 octets (www.example.com is
@@ -146,9 +151,9 @@ escaped:
}
octets[label] = (uint8_t)(length - label);
- } while (left && length < 255);
+ } while (left && length < max_length);
- if (length >= 255)
+ if (length >= max_length)
return -1;
*lengthp = length + 1;
--
2.43.0
diff --git a/simdzone/src/generic/text.h b/simdzone/src/generic/text.h
index 2288c33..5d40062 100644
--- a/simdzone/src/generic/text.h
+++ b/simdzone/src/generic/text.h
@@ -66,7 +66,7 @@ static really_inline int32_t scan_string(
goto escaped;
if (length < 32)
- return (int32_t)count;
+ return octets + count <= limit ? (int32_t)count : -1;
text += count;
wire += count;
--
2.43.0
OpenPGP_0xE5F8F8212F77A498.asc
Description: OpenPGP public key
OpenPGP_signature.asc
Description: OpenPGP digital signature
