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 field

You 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

Attachment: OpenPGP_0xE5F8F8212F77A498.asc
Description: OpenPGP public key

Attachment: OpenPGP_signature.asc
Description: OpenPGP digital signature

Reply via email to