diff --git a/bind-9.18-CVE-2025-40778.patch b/bind-9.18-CVE-2025-40778.patch new file mode 100644 index 0000000..f666c79 --- /dev/null +++ b/bind-9.18-CVE-2025-40778.patch @@ -0,0 +1,469 @@ +From 83cc066e52542d3f52db98b02da33121b316f4be Mon Sep 17 00:00:00 2001 +From: Mark Andrews +Date: Thu, 10 Jul 2025 09:37:36 +1000 +Subject: [PATCH] Tighten restrictions on caching NS RRsets in authority + section + +To prevent certain spoofing attacks, a new check has been added +to the existing rules for whether NS data can be cached: the owner +name of the NS RRset must be an ancestor of the name being queried. + +(cherry picked from commit fa153f791f9324bf84abf8d259e11c0531fe6e25) +(cherry picked from commit 025d61bacd0f57f994a631654aff7a933d89a547) + +Further restrict addresses that are cached when processing referrals + +Use the owner name of the NS record as the bailwick apex name +when determining which additional records to cache, rather than +the name of the delegating zone (or a parent thereof). + +(cherry picked from commit a41054e9e606a61f1b3c8bc0c54e2f1059347165) +(cherry picked from commit cd17dfe696cdf9b8ef23fbc8738de7c79f957846) + +Retry lookups with unsigned DNAME over TCP + +To prevent spoofed unsigned DNAME responses being accepted retry +response with unsigned DNAMEs over TCP if the response is not TSIG +signed or there isn't a good DNS CLIENT COOKIE. + +To prevent test failures, this required adding TCP support to the +ans3 and ans4 servers in the chain system test. + +(cherry picked from commit 2e40705c06831988106335ed77db3cf924d431f6) +(cherry picked from commit 4c6d03b0bb2ffbafcde8e8a5bc0e49908b978a72) +--- + bin/tests/system/chain/ans4/ans.py | 58 +++++++++++++--- + lib/dns/include/dns/message.h | 8 +++ + lib/dns/message.c | 12 ++++ + lib/dns/resolver.c | 108 ++++++++++++++++++++++++----- + lib/dns/validator.c | 33 ++++++--- + 5 files changed, 180 insertions(+), 39 deletions(-) + +diff --git a/bin/tests/system/chain/ans4/ans.py b/bin/tests/system/chain/ans4/ans.py +index 45d6504..e4fc15a 100755 +--- a/bin/tests/system/chain/ans4/ans.py ++++ b/bin/tests/system/chain/ans4/ans.py +@@ -276,16 +276,30 @@ except: port=5300 + try: ctrlport=int(os.environ['EXTRAPORT1']) + except: ctrlport=5300 + +-query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +-query4_socket.bind((ip4, port)) ++query4_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) ++query4_udp.bind((ip4, port)) ++ ++query4_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ++query4_tcp.bind((ip4, port)) ++query4_tcp.listen(1) ++query4_tcp.settimeout(1) + + havev6 = True + try: +- query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) ++ query6_udp = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) ++ try: ++ query6_udp.bind((ip6, port)) ++ except: ++ query6_udp.close() ++ havev6 = False ++ ++ query6_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: +- query6_socket.bind((ip6, port)) ++ query6_tcp.bind((ip4, port)) ++ query6_tcp.listen(1) ++ query6_tcp.settimeout(1) + except: +- query6_socket.close() ++ query6_tcp.close() + havev6 = False + except: + havev6 = False +@@ -310,9 +324,9 @@ print ("Control channel on %s port %d" % (ip4, ctrlport)) + print ("Ctrl-c to quit") + + if havev6: +- input = [query4_socket, query6_socket, ctrl_socket] ++ input = [query4_udp, query4_tcp, query6_udp, query6_tcp, ctrl_socket] + else: +- input = [query4_socket, ctrl_socket] ++ input = [query4_udp, query4_tcp, ctrl_socket] + + while running: + try: +@@ -335,13 +349,37 @@ while running: + break + ctl_channel(msg) + conn.close() +- if s == query4_socket or s == query6_socket: +- print ("Query received on %s" % +- (ip4 if s == query4_socket else ip6)) ++ elif s == query4_udp or s == query6_udp: ++ print("Query received on %s" % (ip4 if s == query4_udp else ip6)) + # Handle incoming queries + msg = s.recvfrom(65535) + rsp = create_response(msg[0]) + if rsp: + s.sendto(rsp, msg[1]) ++ elif s == query4_tcp or s == query6_tcp: ++ try: ++ conn, _ = s.accept() ++ if s == query4_tcp or s == query6_tcp: ++ print( ++ "TCP Query received on %s" % (ip4 if s == query4_tcp else ip6), ++ end=" ", ++ ) ++ # get TCP message length ++ msg = conn.recv(2) ++ if len(msg) != 2: ++ print("couldn't read TCP message length") ++ continue ++ length = struct.unpack(">H", msg[:2])[0] ++ msg = conn.recv(length) ++ if len(msg) != length: ++ print("couldn't read TCP message") ++ continue ++ rsp = create_response(msg) ++ if rsp: ++ conn.send(struct.pack(">H", len(rsp))) ++ conn.send(rsp) ++ conn.close() ++ except socket.error as e: ++ print("error: %s" % str(e)) + if not running: + break +diff --git a/lib/dns/include/dns/message.h b/lib/dns/include/dns/message.h +index 68c13ee..d321a58 100644 +--- a/lib/dns/include/dns/message.h ++++ b/lib/dns/include/dns/message.h +@@ -233,6 +233,7 @@ struct dns_message { + unsigned int cc_bad : 1; + unsigned int tkey : 1; + unsigned int rdclass_set : 1; ++ unsigned int has_dname : 1; + + unsigned int opt_reserved; + unsigned int sig_reserved; +@@ -1449,4 +1450,11 @@ dns_message_clonebuffer(dns_message_t *msg); + * \li msg be a valid message. + */ + ++bool ++dns_message_hasdname(dns_message_t *msg); ++/*%< ++ * Return whether a DNAME was detected in the ANSWER section of a QUERY ++ * message when it was parsed. ++ */ ++ + ISC_LANG_ENDDECLS +diff --git a/lib/dns/message.c b/lib/dns/message.c +index 04315bc..aa434a7 100644 +--- a/lib/dns/message.c ++++ b/lib/dns/message.c +@@ -438,6 +438,7 @@ msginit(dns_message_t *m) { + m->cc_bad = 0; + m->tkey = 0; + m->rdclass_set = 0; ++ m->has_dname = 0; + m->querytsig = NULL; + m->indent.string = "\t"; + m->indent.count = 0; +@@ -1717,6 +1718,11 @@ getsection(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t *dctx, + */ + msg->tsigname->attributes |= DNS_NAMEATTR_NOCOMPRESS; + free_name = false; ++ } else if (rdtype == dns_rdatatype_dname && ++ sectionid == DNS_SECTION_ANSWER && ++ msg->opcode == dns_opcode_query) ++ { ++ msg->has_dname = 1; + } + rdataset = NULL; + +@@ -4750,3 +4756,9 @@ dns_message_clonebuffer(dns_message_t *msg) { + msg->free_query = 1; + } + } ++ ++bool ++dns_message_hasdname(dns_message_t *msg) { ++ REQUIRE(DNS_MESSAGE_VALID(msg)); ++ return msg->has_dname; ++} +diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c +index ad0a1b8..c284b67 100644 +--- a/lib/dns/resolver.c ++++ b/lib/dns/resolver.c +@@ -751,6 +751,7 @@ typedef struct respctx { + bool get_nameservers; /* get a new NS rrset at + * zone cut? */ + bool resend; /* resend this query? */ ++ bool secured; /* message was signed or had a valid cookie */ + bool nextitem; /* invalid response; keep + * listening for the correct one */ + bool truncated; /* response was truncated */ +@@ -7155,7 +7156,8 @@ mark_related(dns_name_t *name, dns_rdataset_t *rdataset, bool external, + * locally served zone. + */ + static inline bool +-name_external(const dns_name_t *name, dns_rdatatype_t type, fetchctx_t *fctx) { ++name_external(const dns_name_t *name, dns_rdatatype_t type, respctx_t *rctx) { ++ fetchctx_t *fctx = rctx->fctx; + isc_result_t result; + dns_forwarders_t *forwarders = NULL; + dns_fixedname_t fixed, zfixed; +@@ -7167,7 +7169,8 @@ name_external(const dns_name_t *name, dns_rdatatype_t type, fetchctx_t *fctx) { + unsigned int labels; + dns_namereln_t rel; + +- apex = ISFORWARDER(fctx->addrinfo) ? fctx->fwdname : &fctx->domain; ++ apex = ISFORWARDER(fctx->addrinfo) ? fctx->fwdname : ++ (rctx->ns_name != NULL) ? rctx->ns_name : &fctx->domain; + + /* + * The name is outside the queried namespace. +@@ -7275,7 +7278,7 @@ check_section(void *arg, const dns_name_t *addname, dns_rdatatype_t type, + result = dns_message_findname(rctx->query->rmessage, section, addname, + dns_rdatatype_any, 0, &name, NULL); + if (result == ISC_R_SUCCESS) { +- external = name_external(name, type, fctx); ++ external = name_external(name, type, rctx); + if (type == dns_rdatatype_a) { + for (rdataset = ISC_LIST_HEAD(name->list); + rdataset != NULL; +@@ -7884,6 +7887,47 @@ betterreferral(respctx_t *rctx) { + return (false); + } + ++static bool ++rctx_need_tcpretry(respctx_t *rctx) { ++ resquery_t *query = rctx->query; ++ if ((rctx->retryopts & DNS_FETCHOPT_TCP) != 0) { ++ /* TCP is already in the retry flags */ ++ return false; ++ } ++ ++ /* ++ * If the message was secured, no need to continue. ++ */ ++ if (rctx->secured) { ++ return false; ++ } ++ ++ /* ++ * Currently the only extra reason why we might need to ++ * retry a UDP response over TCP is a DNAME in the message. ++ */ ++ if (dns_message_hasdname(query->rmessage)) { ++ return true; ++ } ++ ++ return false; ++} ++ ++static isc_result_t ++rctx_tcpretry(respctx_t *rctx) { ++ /* ++ * Do we need to retry a UDP response over TCP? ++ */ ++ if (rctx_need_tcpretry(rctx)) { ++ rctx->retryopts |= DNS_FETCHOPT_TCP; ++ rctx->resend = true; ++ rctx_done(rctx, ISC_R_SUCCESS); ++ return ISC_R_COMPLETE; ++ } ++ ++ return ISC_R_SUCCESS; ++} ++ + /* + * resquery_response(): + * Handles responses received in response to iterative queries sent by +@@ -8069,15 +8113,23 @@ resquery_response(isc_task_t *task, isc_event_t *event) { + * ensured by the dispatch code). + */ + ++ /* ++ * Remember whether this message was signed or had a ++ * valid client cookie; if not, we may need to retry over ++ * TCP later. ++ */ ++ if (query->rmessage->cc_ok || query->rmessage->tsig != NULL || ++ query->rmessage->sig0 != NULL) ++ { ++ rctx.secured = true; ++ } ++ + /* + * If we have had a server cookie and don't get one retry over TCP. + * This may be a misconfigured anycast server or an attempt to send + * a spoofed response. Skip if we have a valid tsig. + */ +- if (dns_message_gettsig(query->rmessage, NULL) == NULL && +- !query->rmessage->cc_ok && !query->rmessage->cc_bad && +- (rctx.retryopts & DNS_FETCHOPT_TCP) == 0) +- { ++ if (!rctx.secured && (rctx.retryopts & DNS_FETCHOPT_TCP) == 0) { + unsigned char cookie[COOKIE_BUFFER_SIZE]; + if (dns_adb_getcookie(fctx->adb, query->addrinfo, cookie, + sizeof(cookie)) > CLIENT_COOKIE_SIZE) +@@ -8103,6 +8155,17 @@ resquery_response(isc_task_t *task, isc_event_t *event) { + */ + } + ++ /* ++ * Check whether we need to retry over TCP for some other reason. ++ */ ++ result = rctx_tcpretry(&rctx); ++ if (result == ISC_R_COMPLETE) { ++ return; ++ } ++ ++ /* ++ * Check for EDNS issues. ++ */ + rctx_edns(&rctx); + + /* +@@ -8830,8 +8893,8 @@ rctx_answer_positive(respctx_t *rctx) { + } + + /* +- * Cache records in the authority section, if +- * there are any suitable for caching. ++ * Cache records in the authority section, if there are ++ * any suitable for caching. + */ + rctx_authority_positive(rctx); + +@@ -8902,7 +8965,7 @@ rctx_answer_scan(respctx_t *rctx) { + /* + * Don't accept DNAME from parent namespace. + */ +- if (name_external(name, dns_rdatatype_dname, fctx)) { ++ if (name_external(name, dns_rdatatype_dname, rctx)) { + continue; + } + +@@ -9200,14 +9263,14 @@ rctx_answer_dname(respctx_t *rctx) { + + /* + * rctx_authority_positive(): +- * Examine the records in the authority section (if there are any) for a +- * positive answer. We expect the names for all rdatasets in this section +- * to be subdomains of the domain being queried; any that are not are +- * skipped. We expect to find only *one* owner name; any names +- * after the first one processed are ignored. We expect to find only +- * rdatasets of type NS, RRSIG, or SIG; all others are ignored. Whatever +- * remains can be cached at trust level authauthority or additional +- * (depending on whether the AA bit was set on the answer). ++ * If a positive answer was received over TCP or secured with a cookie ++ * or TSIG, examine the authority section. We expect names for all ++ * rdatasets in this section to be subdomains of the domain being queried; ++ * any that are not are skipped. We expect to find only *one* owner name; ++ * any names after the first one processed are ignored. We expect to find ++ * only rdatasets of type NS; all others are ignored. Whatever remains can ++ * be cached at trust level authauthority or additional (depending on ++ * whether the AA bit was set on the answer). + */ + static void + rctx_authority_positive(respctx_t *rctx) { +@@ -9215,6 +9278,11 @@ rctx_authority_positive(respctx_t *rctx) { + bool done = false; + isc_result_t result; + ++ /* If it's spoofable, don't cache it. */ ++ if (!rctx->secured && (rctx->query->options & DNS_FETCHOPT_TCP) == 0) { ++ return; ++ } ++ + result = dns_message_firstname(rctx->query->rmessage, + DNS_SECTION_AUTHORITY); + while (!done && result == ISC_R_SUCCESS) { +@@ -9223,7 +9291,9 @@ rctx_authority_positive(respctx_t *rctx) { + dns_message_currentname(rctx->query->rmessage, + DNS_SECTION_AUTHORITY, &name); + +- if (!name_external(name, dns_rdatatype_ns, fctx)) { ++ if (!name_external(name, dns_rdatatype_ns, rctx) && ++ dns_name_issubdomain(&fctx->name, name)) ++ { + dns_rdataset_t *rdataset = NULL; + + /* +diff --git a/lib/dns/validator.c b/lib/dns/validator.c +index e416cc9..37ddd26 100644 +--- a/lib/dns/validator.c ++++ b/lib/dns/validator.c +@@ -1344,17 +1344,17 @@ compute_keytag(dns_rdata_t *rdata) { + /*% + * Is the DNSKEY rrset in val->event->rdataset self-signed? + */ +-static bool ++static isc_result_t + selfsigned_dnskey(dns_validator_t *val) { + dns_rdataset_t *rdataset = val->event->rdataset; + dns_rdataset_t *sigrdataset = val->event->sigrdataset; + dns_name_t *name = val->event->name; + isc_result_t result; + isc_mem_t *mctx = val->view->mctx; +- bool answer = false; ++ bool match = false; + + if (rdataset->type != dns_rdatatype_dnskey) { +- return (false); ++ return DNS_R_NOKEYMATCH; + } + + for (result = dns_rdataset_first(rdataset); result == ISC_R_SUCCESS; +@@ -1390,10 +1390,24 @@ selfsigned_dnskey(dns_validator_t *val) { + continue; + } + ++ /* ++ * If the REVOKE bit is not set we have a ++ * theoretically self-signed DNSKEY RRset; ++ * this will be verified later. ++ * ++ * We don't return the answer yet, though, ++ * because we need to check the remaining keys ++ * and possbly remove them if they're revoked. ++ */ ++ if ((key.flags & DNS_KEYFLAG_REVOKE) == 0) { ++ match = true; ++ break; ++ } ++ + result = dns_dnssec_keyfromrdata(name, &keyrdata, mctx, + &dstkey); + if (result != ISC_R_SUCCESS) { +- continue; ++ break; + } + + result = dns_dnssec_verify(name, rdataset, dstkey, true, +@@ -1404,16 +1418,15 @@ selfsigned_dnskey(dns_validator_t *val) { + continue; + } + +- if ((key.flags & DNS_KEYFLAG_REVOKE) == 0) { +- answer = true; +- continue; +- } +- + dns_view_untrust(val->view, name, &key); + } + } + +- return (answer); ++ if (!match) { ++ return DNS_R_NOKEYMATCH; ++ } ++ ++ return ISC_R_SUCCESS; + } + + /*% +-- +2.51.0 + diff --git a/bind9.16.spec b/bind9.16.spec index aab5054..469541d 100644 --- a/bind9.16.spec +++ b/bind9.16.spec @@ -176,6 +176,10 @@ Patch216: bind-9.18-CVE-2024-11187.patch Patch217: bind-9.16-update-b.root-servers.net.patch # https://gitlab.isc.org/isc-projects/bind9/commit/8330b49fb90bfeae14b47b7983e9459cc2bbaffe Patch225: bind-9.18-CVE-2025-40780.patch +# https://gitlab.isc.org/isc-projects/bind9/commit/025d61bacd0f57f994a631654aff7a933d89a547 +# https://gitlab.isc.org/isc-projects/bind9/commit/cd17dfe696cdf9b8ef23fbc8738de7c79f957846 +# https://gitlab.isc.org/isc-projects/bind9/commit/4c6d03b0bb2ffbafcde8e8a5bc0e49908b978a72 +Patch224: bind-9.18-CVE-2025-40778.patch %{?systemd_ordering} Requires: coreutils @@ -1257,6 +1261,7 @@ fi; %changelog * Wed Oct 29 2025 Petr Menšík - 32:9.16.23-0.22.4 - Prevent cache poisoning due to weak PRNG (CVE-2025-40780) +- Address various spoofing attacks (CVE-2025-40778) * Wed Aug 13 2025 Petr Menšík - Update addresses of b.root-servers.net (RHEL-18449) - 32:9.16.23-0.22.3