Several vulnerabilities were found in NLnet Labs NSD.
We have released version 4.14.3 as a security release today,Thursday 25 June, with the fixes to these issues.

The overview of the vulnerabilities with a brief description is:

CVE-2026-12244 - severity: HIGH
Heap overflow and crash with crafted SVCB RR

CVE-2026-12245 - severity: HIGH
Denial of DNS over TLS service by any DoT client

CVE-2026-12246 - severity: HIGH
Out of bounds stack write with crafted APL RR

CVE-2026-12490 - severity: HIGH
Bypass of client certificate verification with transfer over TLS

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.14.3.diff).

The patches are tested to apply/work on 4.14.2


Best regards,
-- Willem, on behalf of the NSD team.
The CVE number for this vulnerability is CVE-2026-12244

== Summary
A specially crafted SVCB RR can cause a heap overflow of up to 65509 attacker
controlled bytes.


== Affected products
NSD from and including version 4.14.0 up to and including version 4.14.2


== Description
If NSD is configured as secondary for a zone, the primary of that zone can
crash NSD with an AXFR containing a DNS message with a special crafted SVCB RR
with an rdata size of 65512, that let's an (uint16_t) variable that is used to
allocate space needed for the RR wrap (because total size > 65535), causing a
heap overflow. The attacker can perform a controlled (RCE class) head write of
up to 65509 bytes

Even though the data is from a configured primary inside NSD's trust boundary,
we do consider the risk significant enough for multi-tenant secondary DNS
deployments, given the potential severity of the attack.

== Mitigation

=== Downloading patched version
NSD 4.14.3 is released with the patch
https://nlnetlabs.nl/downloads/nsd/nsd-4.14.3.tar.gz

=== Applying the patch manually
For NSD 4.14.2 the patch is:
https://nlnetlabs.nl/downloads/nsd/patch_CVE-2026-12244.diff

Apply the patch on the nsd source directory with:
    patch -p1 < patch_CVE-2026-12244.diff
then run 'make install' to install nsd.

The patch is tested to work on nsd 4.14.2.


== Acknowledgments
We would like to thank Qifan Zhang from Palo Alto Networks for discovering and
responsibly disclosing the vulnerability.
diff --git a/rdata.c b/rdata.c
index 69695d7e..0dc4f5a5 100644
--- a/rdata.c
+++ b/rdata.c
@@ -3282,7 +3282,7 @@ read_svcb_rdata(struct domain_table *domains, uint16_t rdlength,
 	struct domain *domain;
 	struct dname_buffer target;
 	uint16_t length = 2, svcparams_length = 0;
-	uint16_t size;
+	size_t size;
 	const size_t mark = buffer_position(packet);
 
 	/* short + name + svc_params */
-- 
2.43.0

The CVE number for this vulnerability is CVE-2026-12245

== Summary
If NSD is configured with DNS over TLS, a client that performs a TLS action,
closing the connection early, causes a crash and restart of the server process.
An attacker can keep all children in a crash-restart loop denying DoT service.


== Affected products
NSD from and including version 4.13.0 up to and including version 4.14.2


== Description
NSD from version 4.13.0 has a heap use-after-free bug in logging errors on TLS
connections, causing a crash of the server process, which can be triggered
trivially by sending a DNS query over a DoT connection, and closing the
connection without reading the response.

Any client with access to the DoT port (853) can trigger this. Even though a
new server process will be immediately reforked to replace the crashed one,
an attacker can keep all children in a crash-restart loop denying DoT service.


== Mitigation

=== Downloading patched version
NSD 4.14.3 is released with the patch
https://nlnetlabs.nl/downloads/nsd/nsd-4.14.3.tar.gz

=== Applying the patch manually
For NSD 4.14.2 the patch is:
https://nlnetlabs.nl/downloads/nsd/patch_CVE-2026-12245.diff

Apply the patch on the nsd source directory with:
    patch -p1 < patch_CVE-2026-12245.diff
then run 'make install' to install nsd.

The patch is tested to work on nsd 4.14.2.


== Acknowledgments
We would like to thank Qifan Zhang from Palo Alto Networks for discovering and
responsibly disclosing the vulnerability.
diff --git a/server.c b/server.c
index 756b51ea..e30c4ef2 100644
--- a/server.c
+++ b/server.c
@@ -5411,7 +5411,6 @@ handle_tls_writing(int fd, short event, void* arg)
 			data->shake_state = tls_hs_read_event;
 			tcp_handler_setup_event(data, handle_tls_reading, fd, EV_PERSIST | EV_READ | EV_TIMEOUT);
 		} else if(want != SSL_ERROR_WANT_WRITE) {
-			cleanup_tcp_handler(data);
 			{
 				char client_ip[128], e[188];
 				if(data->query) {
@@ -5423,6 +5422,7 @@ handle_tls_writing(int fd, short event, void* arg)
 					client_ip, "SSL_write error");
 				log_crypto_err(e);
 			}
+			cleanup_tcp_handler(data);
 		}
 		return;
 	}
-- 
2.54.0

The CVE number for this vulnerability is CVE-2026-12246

== Summary
The RR type APL rdata address, if too large, causes out of bounds write on the
stack, when the zonefile is written out.


== Affected products
NSD from and including version 4.14.0 up to and including version 4.14.2


== Description
NSD version 4.14.0 introduced a bug where a specially crafted APL RR, with an
adflength larger than permitted for the address family will overwrite the
stack when the zone is written to disk, with a maximum of 111 attacker
controlled bytes.

Even though the data is from a configured primary inside NSD's trust boundary,
we do consider the risk significant enough for multi-tenant secondary DNS
deployments, where a primary could introduce the rogue APL with the secondary
not noticing or only after the fact.


== Mitigation

=== Downloading patched version
NSD 4.14.3 is released with the patch
https://nlnetlabs.nl/downloads/nsd/nsd-4.14.3.tar.gz

=== Applying the patch manually
For NSD 4.14.2 the patch is:
https://nlnetlabs.nl/downloads/nsd/patch_CVE-2026-12246.diff

Apply the patch on the nsd source directory with:
    patch -p1 < patch_CVE-2026-12246.diff
then run 'make install' to install nsd.

The patch is tested to work on nsd 4.14.2.


== Acknowledgments
We would like to thank Qifan Zhang from Palo Alto Networks, Haruki Oyama from
Waseda University and zhangph for discovering and responsibly disclosing the
vulnerability.
diff --git a/rdata.c b/rdata.c
index 7b7ae00f..dc00d469 100644
--- a/rdata.c
+++ b/rdata.c
@@ -2560,7 +2560,10 @@ read_apl_rdata(struct domain_table *domains, uint16_t rdlength,
 		return MALFORMED;
 	while (rdlength - length >= 4) {
 		uint8_t afdlength = rdata[length + 3] & APL_LENGTH_MASK;
-		if (rdlength - (length + 4) < afdlength)
+		uint16_t afam = read_uint16(rdata + length);
+		if (rdlength - (length + 4) < afdlength ||
+			(afam == 1 && afdlength > 4) ||
+			(afam == 2 && afdlength > 16))
 			return MALFORMED;
 		length += 4 + afdlength;
 	}
@@ -2600,14 +2603,22 @@ print_apl(struct buffer *output, size_t rdlength, const uint8_t *rdata,
 	af = -1;
 
 	switch (address_family) {
-	case 1: af = AF_INET; break;
-	case 2: af = AF_INET6; break;
+	case 1: af = AF_INET;
+		if(length > 4)
+			return 0;
+		break;
+	case 2: af = AF_INET6;
+		if(length > 16)
+			return 0;
+		break;
 	}
 
 	if (af == -1 || size - 4 < length)
 		return 0;
 
 	memset(address, 0, sizeof(address));
+	if(length > sizeof(address))
+		return 0;
 	memmove(address, rdata + *offset + 4, length);
 
 	if (!inet_ntop(af, address, text_address, sizeof(text_address)))
-- 
2.54.0

The CVE number for this vulnerability is CVE-2026-12490

== Summary
Secondaries authenticated by a client certificate to transfer a zone over TLS,
can bypass verification by transferring over TCP.


== Affected products
NSD up to and including version 4.14.2


== Description
When a "provide-xfr" is given with a "tls-auth-name", a secondary requesting a
transfer should provide a client certificate with that name. However, no client
certificate is needed when the request comes in over TLS over the regular
"tls-port" (and not the "tls-auth-port") or over over TCP over the regular port,
when the other conditions of the "provide-xfr" rule match.

The transfer security restrictions for client certificates can be bypassed
completely if the attacker can match the other access control conditions, and
the "tls-auth-xfr-only" option is not explicitly set to "yes" (which it by
default is not)


== Mitigation

=== Downloading patched version
NSD 4.14.3 is released with the patch
https://nlnetlabs.nl/downloads/nsd/nsd-4.14.3.tar.gz

=== Applying the patch manually
For NSD 4.14.2 the patch is:
https://nlnetlabs.nl/downloads/nsd/patch_CVE-2026-12490.diff

Apply the patch on the nsd source directory with:
    patch -p1 < patch_CVE-2026-12490.diff
then run 'make install' to install nsd.

The patch is tested to work on nsd 4.14.2.


== Acknowledgments
We would like to thank Qifan Zhang from Palo Alto Networks for discovering and
responsibly disclosing the vulnerability.
diff --git a/nsd.conf.5.in b/nsd.conf.5.in
index 8aad818c..b0df0bc2 100644
--- a/nsd.conf.5.in
+++ b/nsd.conf.5.in
@@ -1037,7 +1037,7 @@ SAN (Subject Alternative Name) DNS entry or CN (Common Name) entry equal to
 .BR auth-domain-name
 of the defined
 .BR tls-auth .
-The certificate validify is also verified with
+The certificate validity is also verified with
 .BR tls-cert-bundle .
 If authentication of the secondary, based on the specified tls-auth authentication
 information, fails the XFR zone transfer will be refused. If the connection is performed
diff --git a/options.c b/options.c
index 950a949e..a6005900 100644
--- a/options.c
+++ b/options.c
@@ -333,6 +333,9 @@ parse_options_file(struct nsd_options* opt, const char* file,
 			                tls_auth_options_find(opt, acl->tls_auth_name)))
 				    c_error("tls_auth %s in pattern %s could not be found",
 						acl->tls_auth_name, pat->pname);
+				else if (!opt->tls_auth_port)
+				    c_warning("provide-xfr has a tls-auth-name,"
+				         " but no tls-auth-port is configured");
 			}
 			if(acl->nokey || acl->blocked)
 				continue;
@@ -959,10 +962,11 @@ zone_list_close(struct nsd_options* opt)
 }
 
 static void
-c_error_va_list_pos(int showpos, const char* fmt, va_list args)
+c_error_va_list_pos(int showpos, int is_error, const char* fmt, va_list args)
 {
 	char* at = NULL;
-	cfg_parser->errors++;
+	if(is_error)
+		cfg_parser->errors++;
 	if(showpos && c_text && c_text[0]!=0) {
 		at = c_text;
 	}
@@ -975,7 +979,8 @@ c_error_va_list_pos(int showpos, const char* fmt, va_list args)
 			snprintf(m, sizeof(m), "at '%s': ", at);
 			(*cfg_parser->err)(cfg_parser->err_arg, m);
 		}
-		(*cfg_parser->err)(cfg_parser->err_arg, "error: ");
+		(*cfg_parser->err)(cfg_parser->err_arg,
+			is_error ? "error: " : "warning: ");
 		vsnprintf(m, sizeof(m), fmt, args);
 		(*cfg_parser->err)(cfg_parser->err_arg, m);
 		(*cfg_parser->err)(cfg_parser->err_arg, "\n");
@@ -983,7 +988,7 @@ c_error_va_list_pos(int showpos, const char* fmt, va_list args)
 	}
         fprintf(stderr, "%s:%d: ", cfg_parser->filename, cfg_parser->line);
 	if(at) fprintf(stderr, "at '%s': ", at);
-	fprintf(stderr, "error: ");
+	fprintf(stderr, is_error ? "error: " : "warning: ");
 	vfprintf(stderr, fmt, args);
 	fprintf(stderr, "\n");
 }
@@ -999,7 +1004,22 @@ c_error(const char *fmt, ...)
 	}
 
 	va_start(ap, fmt);
-	c_error_va_list_pos(showpos, fmt, ap);
+	c_error_va_list_pos(showpos, 1, fmt, ap);
+	va_end(ap);
+}
+
+void
+c_warning(const char *fmt, ...)
+{
+	va_list ap;
+	int showpos = 0;
+
+	if (strcmp(fmt, "syntax error") == 0 || strcmp(fmt, "parse error") == 0) {
+		showpos = 1;
+	}
+
+	va_start(ap, fmt);
+	c_error_va_list_pos(showpos, 0, fmt, ap);
 	va_end(ap);
 }
 
@@ -1993,6 +2013,16 @@ acl_check_incoming(struct acl_options* acl, struct query* q,
 		DEBUG(DEBUG_XFRD,2, (LOG_INFO, "testing acl %s %s",
 			acl->ip_address_spec, acl->nokey?"NOKEY":
 			(acl->blocked?"BLOCKED":acl->key_name)));
+#endif
+#ifdef HAVE_SSL
+		if (acl->tls_auth_name && !q->tls_auth) {
+			/* the acl requires a TLS client cert with name, but
+			 * the connection did not came over a "tls-auth-port:"
+			 */
+			number++;
+			acl = acl->next;
+			continue;
+		}
 #endif
 		if(acl_addr_matches(acl, q) && acl_key_matches(acl, q)) {
 			if(!match)
@@ -2008,7 +2038,7 @@ acl_check_incoming(struct acl_options* acl, struct query* q,
 		}
 #ifdef HAVE_SSL
 		/* we are in a acl with tls_auth */
-		if (acl->tls_auth_name && q->tls_auth) {
+		if (acl->tls_auth_name) {
 			/* we have auth_domain_name in tls_auth */
 			if (acl->tls_auth_options && acl->tls_auth_options->auth_domain_name) {
 				if (!acl_tls_hostname_matches(q->tls_auth, acl->tls_auth_options->auth_domain_name)) {
diff --git a/options.h b/options.h
index 5cd1cda5..eda716f2 100644
--- a/options.h
+++ b/options.h
@@ -641,6 +641,7 @@ const char* config_make_zonefile(struct zone_options* zone, struct nsd* nsd);
 
 /* parsing helpers */
 void c_error(const char* msg, ...) ATTR_FORMAT(printf, 1,2);
+void c_warning(const char* msg, ...) ATTR_FORMAT(printf, 1,2);
 int c_wrap(void);
 struct acl_options* parse_acl_info(region_type* region, char* ip,
 	const char* key);
-- 
2.43.0

diff --git a/nsd.conf.5.in b/nsd.conf.5.in
index 8aad818c..b0df0bc2 100644
--- a/nsd.conf.5.in
+++ b/nsd.conf.5.in
@@ -1037,7 +1037,7 @@ SAN (Subject Alternative Name) DNS entry or CN (Common Name) entry equal to
 .BR auth-domain-name
 of the defined
 .BR tls-auth .
-The certificate validify is also verified with
+The certificate validity is also verified with
 .BR tls-cert-bundle .
 If authentication of the secondary, based on the specified tls-auth authentication
 information, fails the XFR zone transfer will be refused. If the connection is performed
diff --git a/options.c b/options.c
index 950a949e..a6005900 100644
--- a/options.c
+++ b/options.c
@@ -333,6 +333,9 @@ parse_options_file(struct nsd_options* opt, const char* file,
 			                tls_auth_options_find(opt, acl->tls_auth_name)))
 				    c_error("tls_auth %s in pattern %s could not be found",
 						acl->tls_auth_name, pat->pname);
+				else if (!opt->tls_auth_port)
+				    c_warning("provide-xfr has a tls-auth-name,"
+				         " but no tls-auth-port is configured");
 			}
 			if(acl->nokey || acl->blocked)
 				continue;
@@ -959,10 +962,11 @@ zone_list_close(struct nsd_options* opt)
 }
 
 static void
-c_error_va_list_pos(int showpos, const char* fmt, va_list args)
+c_error_va_list_pos(int showpos, int is_error, const char* fmt, va_list args)
 {
 	char* at = NULL;
-	cfg_parser->errors++;
+	if(is_error)
+		cfg_parser->errors++;
 	if(showpos && c_text && c_text[0]!=0) {
 		at = c_text;
 	}
@@ -975,7 +979,8 @@ c_error_va_list_pos(int showpos, const char* fmt, va_list args)
 			snprintf(m, sizeof(m), "at '%s': ", at);
 			(*cfg_parser->err)(cfg_parser->err_arg, m);
 		}
-		(*cfg_parser->err)(cfg_parser->err_arg, "error: ");
+		(*cfg_parser->err)(cfg_parser->err_arg,
+			is_error ? "error: " : "warning: ");
 		vsnprintf(m, sizeof(m), fmt, args);
 		(*cfg_parser->err)(cfg_parser->err_arg, m);
 		(*cfg_parser->err)(cfg_parser->err_arg, "\n");
@@ -983,7 +988,7 @@ c_error_va_list_pos(int showpos, const char* fmt, va_list args)
 	}
         fprintf(stderr, "%s:%d: ", cfg_parser->filename, cfg_parser->line);
 	if(at) fprintf(stderr, "at '%s': ", at);
-	fprintf(stderr, "error: ");
+	fprintf(stderr, is_error ? "error: " : "warning: ");
 	vfprintf(stderr, fmt, args);
 	fprintf(stderr, "\n");
 }
@@ -999,7 +1004,22 @@ c_error(const char *fmt, ...)
 	}
 
 	va_start(ap, fmt);
-	c_error_va_list_pos(showpos, fmt, ap);
+	c_error_va_list_pos(showpos, 1, fmt, ap);
+	va_end(ap);
+}
+
+void
+c_warning(const char *fmt, ...)
+{
+	va_list ap;
+	int showpos = 0;
+
+	if (strcmp(fmt, "syntax error") == 0 || strcmp(fmt, "parse error") == 0) {
+		showpos = 1;
+	}
+
+	va_start(ap, fmt);
+	c_error_va_list_pos(showpos, 0, fmt, ap);
 	va_end(ap);
 }
 
@@ -1993,6 +2013,16 @@ acl_check_incoming(struct acl_options* acl, struct query* q,
 		DEBUG(DEBUG_XFRD,2, (LOG_INFO, "testing acl %s %s",
 			acl->ip_address_spec, acl->nokey?"NOKEY":
 			(acl->blocked?"BLOCKED":acl->key_name)));
+#endif
+#ifdef HAVE_SSL
+		if (acl->tls_auth_name && !q->tls_auth) {
+			/* the acl requires a TLS client cert with name, but
+			 * the connection did not came over a "tls-auth-port:"
+			 */
+			number++;
+			acl = acl->next;
+			continue;
+		}
 #endif
 		if(acl_addr_matches(acl, q) && acl_key_matches(acl, q)) {
 			if(!match)
@@ -2008,7 +2038,7 @@ acl_check_incoming(struct acl_options* acl, struct query* q,
 		}
 #ifdef HAVE_SSL
 		/* we are in a acl with tls_auth */
-		if (acl->tls_auth_name && q->tls_auth) {
+		if (acl->tls_auth_name) {
 			/* we have auth_domain_name in tls_auth */
 			if (acl->tls_auth_options && acl->tls_auth_options->auth_domain_name) {
 				if (!acl_tls_hostname_matches(q->tls_auth, acl->tls_auth_options->auth_domain_name)) {
diff --git a/options.h b/options.h
index 5cd1cda5..eda716f2 100644
--- a/options.h
+++ b/options.h
@@ -641,6 +641,7 @@ const char* config_make_zonefile(struct zone_options* zone, struct nsd* nsd);
 
 /* parsing helpers */
 void c_error(const char* msg, ...) ATTR_FORMAT(printf, 1,2);
+void c_warning(const char* msg, ...) ATTR_FORMAT(printf, 1,2);
 int c_wrap(void);
 struct acl_options* parse_acl_info(region_type* region, char* ip,
 	const char* key);
diff --git a/rdata.c b/rdata.c
index 69695d7e..a3be91a6 100644
--- a/rdata.c
+++ b/rdata.c
@@ -2560,7 +2560,10 @@ read_apl_rdata(struct domain_table *domains, uint16_t rdlength,
 		return MALFORMED;
 	while (rdlength - length >= 4) {
 		uint8_t afdlength = rdata[length + 3] & APL_LENGTH_MASK;
-		if (rdlength - (length + 4) < afdlength)
+		uint16_t afam = read_uint16(rdata + length);
+		if (rdlength - (length + 4) < afdlength ||
+			(afam == 1 && afdlength > 4) ||
+			(afam == 2 && afdlength > 16))
 			return MALFORMED;
 		length += 4 + afdlength;
 	}
@@ -2600,14 +2603,22 @@ print_apl(struct buffer *output, size_t rdlength, const uint8_t *rdata,
 	af = -1;
 
 	switch (address_family) {
-	case 1: af = AF_INET; break;
-	case 2: af = AF_INET6; break;
+	case 1: af = AF_INET;
+		if(length > 4)
+			return 0;
+		break;
+	case 2: af = AF_INET6;
+		if(length > 16)
+			return 0;
+		break;
 	}
 
 	if (af == -1 || size - 4 < length)
 		return 0;
 
 	memset(address, 0, sizeof(address));
+	if(length > sizeof(address))
+		return 0;
 	memmove(address, rdata + *offset + 4, length);
 
 	if (!inet_ntop(af, address, text_address, sizeof(text_address)))
@@ -3282,7 +3293,7 @@ read_svcb_rdata(struct domain_table *domains, uint16_t rdlength,
 	struct domain *domain;
 	struct dname_buffer target;
 	uint16_t length = 2, svcparams_length = 0;
-	uint16_t size;
+	size_t size;
 	const size_t mark = buffer_position(packet);
 
 	/* short + name + svc_params */
diff --git a/server.c b/server.c
index ee67c035..eb276cc9 100644
--- a/server.c
+++ b/server.c
@@ -5409,7 +5409,6 @@ handle_tls_writing(int fd, short event, void* arg)
 			data->shake_state = tls_hs_read_event;
 			tcp_handler_setup_event(data, handle_tls_reading, fd, EV_PERSIST | EV_READ | EV_TIMEOUT);
 		} else if(want != SSL_ERROR_WANT_WRITE) {
-			cleanup_tcp_handler(data);
 			{
 				char client_ip[128], e[188];
 				if(data->query) {
@@ -5421,6 +5420,7 @@ handle_tls_writing(int fd, short event, void* arg)
 					client_ip, "SSL_write error");
 				log_crypto_err(e);
 			}
+			cleanup_tcp_handler(data);
 		}
 		return;
 	}

Reply via email to