The CVE number for this vulnerability will be CVE-2026-10846
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Score: 8.2, Severity: High
== Summary
When ldns is used by applications for (stub) resolving, it does not
sufficiently verify that received responses belong to a sent query.
== Affected products
ldns 1.2.0 up to and including 1.9.0
== Description
NLnet Labs ldns 1.2.0 up to and including versions 1.9.0, when used in
applications as (stub) resolver over UDP, lacks matching the query
destination address and port with the response source address and port.
Furthermore not the query ID, neither the question of the query is
matched with that of the response. This makes applications, that use
ldns for (stub) resolver functionality, vulnerable for off-path
poisoning attacks.
The drill tool, which is shipped with ldns and uses ldns for stub
resolving, inherently suffers from this vulnerability.
== Solution
Use the patched version of ldns 1.9.1 that has been releases Wednesday
the 10th of June 2026
Or apply the patch manually. For ldns 1.9.0 the patch is attached as
patch_cve_2026-10846.diff
Apply the patch on ldns source directory with:
'patch -p0 < patch_cve_2026-10846.diff'
then run 'make install' to install ldns.
== Acknowledgments
We would like to thank Pablo Ruiz from 'codecome.ai' for finding and
reporting this vulnerability.diff --git a/error.c b/error.c
index 5723aea9..4fc05d6d 100644
--- a/error.c
+++ b/error.c
@@ -191,6 +191,12 @@ ldns_lookup_table ldns_error_str[] = {
"at least 2 bytes of option data" },
{ LDNS_STATUS_EQUAL_RR,
"An identical RR already existed in the zone" },
+ { LDNS_STATUS_ID_DID_NOT_MATCH,
+ "Response ID did not match the query ID" },
+ { LDNS_STATUS_QDCOUNT_MUST_BE_ONE,
+ "The query section MUST contain exactly one question" },
+ { LDNS_STATUS_QUERY_DID_NOT_MATCH,
+ "The question in the response did not match the query" },
{ 0, NULL }
};
diff --git a/ldns/error.h b/ldns/error.h
index a76eb2ec..41d64cc0 100644
--- a/ldns/error.h
+++ b/ldns/error.h
@@ -144,7 +144,10 @@ enum ldns_enum_status {
LDNS_STATUS_INVALID_SVCPARAM_VALUE,
LDNS_STATUS_NOT_EDE,
LDNS_STATUS_EDE_OPTION_MALFORMED,
- LDNS_STATUS_EQUAL_RR
+ LDNS_STATUS_EQUAL_RR,
+ LDNS_STATUS_ID_DID_NOT_MATCH,
+ LDNS_STATUS_QDCOUNT_MUST_BE_ONE,
+ LDNS_STATUS_QUERY_DID_NOT_MATCH
};
typedef enum ldns_enum_status ldns_status;
diff --git a/net.c b/net.c
index e944d018..4c1f4054 100644
--- a/net.c
+++ b/net.c
@@ -441,6 +441,50 @@ ldns_udp_bgsend2(ldns_buffer *qbin,
return ldns_udp_bgsend_from(qbin, to, tolen, NULL, 0, timeout);
}
+/** helper sockaddr compare function. returns -1, 0 or 1. */
+static int
+ldns_sockaddr_cmp(const struct sockaddr_storage* addr1, socklen_t len1,
+ const struct sockaddr_storage* addr2, socklen_t len2)
+{
+ struct sockaddr_in* p1_in = (struct sockaddr_in*)addr1;
+ struct sockaddr_in* p2_in = (struct sockaddr_in*)addr2;
+ struct sockaddr_in6* p1_in6 = (struct sockaddr_in6*)addr1;
+ struct sockaddr_in6* p2_in6 = (struct sockaddr_in6*)addr2;
+ if(len1 < len2)
+ return -1;
+ if(len1 > len2)
+ return 1;
+ assert(len1 == len2);
+ if( p1_in->sin_family < p2_in->sin_family)
+ return -1;
+ if( p1_in->sin_family > p2_in->sin_family)
+ return 1;
+ assert( p1_in->sin_family == p2_in->sin_family );
+ /* compare ip4 */
+ if( p1_in->sin_family == AF_INET ) {
+ /* just order it, ntohs not required */
+ if(p1_in->sin_port < p2_in->sin_port)
+ return -1;
+ if(p1_in->sin_port > p2_in->sin_port)
+ return 1;
+ assert(p1_in->sin_port == p2_in->sin_port);
+ return memcmp(&p1_in->sin_addr, &p2_in->sin_addr,
+ sizeof(p1_in->sin_addr));
+ } else if (p1_in6->sin6_family == AF_INET6) {
+ /* just order it, ntohs not required */
+ if(p1_in6->sin6_port < p2_in6->sin6_port)
+ return -1;
+ if(p1_in6->sin6_port > p2_in6->sin6_port)
+ return 1;
+ assert(p1_in6->sin6_port == p2_in6->sin6_port);
+ return memcmp(&p1_in6->sin6_addr, &p2_in6->sin6_addr,
+ sizeof(p1_in6->sin6_addr));
+ } else {
+ /* eek unknown type, perform this comparison for sanity. */
+ return memcmp(addr1, addr2, len1);
+ }
+}
+
static ldns_status
ldns_udp_send_from(uint8_t **result, ldns_buffer *qbin,
const struct sockaddr_storage *to , socklen_t tolen,
@@ -449,6 +493,8 @@ ldns_udp_send_from(uint8_t **result, ldns_buffer *qbin,
{
int sockfd;
uint8_t *answer;
+ struct sockaddr_storage reply_addr;
+ socklen_t reply_addr_len;
sockfd = ldns_udp_bgsend_from(qbin, to, tolen, from, fromlen, timeout);
@@ -467,13 +513,21 @@ ldns_udp_send_from(uint8_t **result, ldns_buffer *qbin,
* but returns a 'NETWORK_ERROR' much like a timeout. */
ldns_sock_nonblock(sockfd);
- answer = ldns_udp_read_wire(sockfd, answer_size, NULL, NULL);
+ reply_addr_len = sizeof(reply_addr);
+ memset(&reply_addr, 0, reply_addr_len);
+ answer = ldns_udp_read_wire(sockfd, answer_size, &reply_addr,
+ &reply_addr_len);
close_socket(sockfd);
if (!answer) {
/* oops */
return LDNS_STATUS_NETWORK_ERR;
}
+ /* Check that the reply came from the to addr. */
+ if(ldns_sockaddr_cmp(to, tolen, &reply_addr, reply_addr_len) != 0) {
+ free(answer);
+ return LDNS_STATUS_NETWORK_ERR;
+ }
*result = answer;
return LDNS_STATUS_OK;
@@ -512,6 +566,10 @@ ldns_send_buffer(ldns_pkt **result, ldns_resolver *r, ldns_buffer *qb, ldns_rdf
assert(r != NULL);
+ /* The query should at least have one question */
+ if(ldns_buffer_limit(qb) < 6 || ldns_buffer_read_u16_at(qb, 4) != 1)
+ return LDNS_STATUS_QDCOUNT_MUST_BE_ONE;
+
status = LDNS_STATUS_OK;
rtt = ldns_resolver_rtt(r);
ns_array = ldns_resolver_nameservers(r);
@@ -599,6 +657,16 @@ ldns_send_buffer(ldns_pkt **result, ldns_resolver *r, ldns_buffer *qb, ldns_rdf
ldns_resolver_set_nameserver_rtt(r, i, LDNS_RESOLV_RTT_INF);
status = send_status;
}
+ if(reply_bytes && ldns_buffer_limit(qb) >= 2) {
+ uint16_t txid = ldns_buffer_read_u16_at(qb, 0);
+ if(reply_size < 2 ||
+ ldns_read_uint16(reply_bytes) != txid) {
+ status = LDNS_STATUS_ID_DID_NOT_MATCH;
+ LDNS_FREE(reply_bytes);
+ reply_bytes = NULL;
+ reply_size = 0;
+ }
+ }
/* obey the fail directive */
if (!reply_bytes) {
@@ -608,7 +676,7 @@ ldns_send_buffer(ldns_pkt **result, ldns_resolver *r, ldns_buffer *qb, ldns_rdf
LDNS_FREE(src);
}
LDNS_FREE(ns);
- return LDNS_STATUS_ERR;
+ return status ? status : LDNS_STATUS_ERR;
} else {
LDNS_FREE(ns);
continue;
@@ -670,6 +738,26 @@ ldns_send_buffer(ldns_pkt **result, ldns_resolver *r, ldns_buffer *qb, ldns_rdf
#endif /* HAVE_SSL */
LDNS_FREE(reply_bytes);
+ if (reply) {
+ ldns_pkt *query = NULL;
+
+ if(ldns_pkt_qdcount(reply) != 1) {
+ status = LDNS_STATUS_QDCOUNT_MUST_BE_ONE;
+ ldns_pkt_free(reply);
+ reply = NULL;
+
+ } else if(ldns_wire2pkt(&query
+ , ldns_buffer_begin(qb)
+ , ldns_buffer_position(qb)) != LDNS_STATUS_OK
+ || ldns_pkt_qdcount(query) != 1
+ || ldns_rr_compare(ldns_rr_list_rr(ldns_pkt_question(query),0)
+ ,ldns_rr_list_rr(ldns_pkt_question(reply),0))){
+ status = LDNS_STATUS_QUERY_DID_NOT_MATCH;
+ ldns_pkt_free(reply);
+ reply = NULL;
+ }
+ ldns_pkt_free(query);
+ }
if (result) {
*result = reply;
}