mirror of
https://github.com/isc-projects/bind9.git
synced 2026-06-13 19:50:02 -04:00
[CVE-2026-5950] sec: usr: Avoid unbounded recursion loop
A bug during bad server handling could cause the resolver to enter an infinite loop, continuously sending queries to an upstream server with no exit condition, until the resolver query timeout was hit. This has been fixed. ISC would like to thank Billy Baraja (BielraX) for bringing this issue to our attention. Closes isc-projects/bind9#5804 Merge branch '5804-resend-loop' into 'security-main' See merge request isc-private/bind9!985
This commit is contained in:
commit
5319c21761
5 changed files with 229 additions and 30 deletions
126
bin/tests/system/resend_loop/ans3/ans.py
Normal file
126
bin/tests/system/resend_loop/ans3/ans.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# See the COPYRIGHT file distributed with this work for additional
|
||||
# information regarding copyright ownership.
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
import dns.edns
|
||||
import dns.name
|
||||
import dns.rcode
|
||||
import dns.rdatatype
|
||||
import dns.rrset
|
||||
|
||||
from isctest.asyncserver import (
|
||||
AsyncDnsServer,
|
||||
DnsResponseSend,
|
||||
QueryContext,
|
||||
ResponseHandler,
|
||||
)
|
||||
|
||||
|
||||
def _get_cookie(qctx: QueryContext):
|
||||
for o in qctx.query.options:
|
||||
if o.otype == dns.edns.OptionType.COOKIE:
|
||||
cookie = o
|
||||
try:
|
||||
if len(cookie.server) == 0:
|
||||
cookie.server = b"\x11\x22\x33\x44\x55\x66\x77\x88"
|
||||
except AttributeError: # dnspython<2.7.0 compat
|
||||
if len(o.data) == 8:
|
||||
cookie.data *= 2
|
||||
|
||||
return cookie
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class PrimeHandler(ResponseHandler):
|
||||
"""
|
||||
Specifically handle priming query for "." NS (type 2)
|
||||
"""
|
||||
|
||||
def match(self, qctx: QueryContext) -> bool:
|
||||
return len(qctx.qname.labels) == 0 and qctx.qtype == dns.rdatatype.NS
|
||||
|
||||
async def get_responses(
|
||||
self, qctx: QueryContext
|
||||
) -> AsyncGenerator[DnsResponseSend, None]:
|
||||
|
||||
ns_rrset = dns.rrset.from_text(
|
||||
".", dns.rdatatype.NS, qctx.qclass, "a.root-servers.nil."
|
||||
)
|
||||
a_rrset = dns.rrset.from_text(
|
||||
"a.root-servers.nil.", dns.rdatatype.A, qctx.qclass, "10.53.0.3"
|
||||
)
|
||||
|
||||
response = qctx.prepare_new_response(with_zone_data=False)
|
||||
response.set_rcode(dns.rcode.NOERROR)
|
||||
response.answer.append(ns_rrset)
|
||||
response.additional.append(a_rrset)
|
||||
|
||||
yield DnsResponseSend(response, authoritative=True)
|
||||
|
||||
|
||||
class CookieHandler(ResponseHandler):
|
||||
def match(self, qctx: QueryContext) -> bool:
|
||||
example = dns.name.from_text("example")
|
||||
return qctx.qname.is_subdomain(example)
|
||||
|
||||
async def get_responses(
|
||||
self, qctx: QueryContext
|
||||
) -> AsyncGenerator[DnsResponseSend, None]:
|
||||
|
||||
qctx.prepare_new_response()
|
||||
|
||||
# Check for client cookie
|
||||
cookie = _get_cookie(qctx)
|
||||
|
||||
# If missing cookie entirely, just return SERVFAIL
|
||||
if cookie is None:
|
||||
qctx.response.set_rcode(dns.rcode.SERVFAIL)
|
||||
yield DnsResponseSend(qctx.response, authoritative=True)
|
||||
|
||||
# If there is a client cookie, mock BADCOOKIE to trigger
|
||||
# the resend loop logic.
|
||||
qctx.response.use_edns(options=[cookie])
|
||||
qctx.response.set_rcode(dns.rcode.BADCOOKIE)
|
||||
yield DnsResponseSend(qctx.response, authoritative=True)
|
||||
|
||||
|
||||
class NoErrorHandler(ResponseHandler):
|
||||
"""
|
||||
If the query is NOT a subdomain of example, respond with standard NOERROR empty answer
|
||||
"""
|
||||
|
||||
async def get_responses(
|
||||
self, qctx: QueryContext
|
||||
) -> AsyncGenerator[DnsResponseSend, None]:
|
||||
|
||||
qctx.prepare_new_response()
|
||||
qctx.response.set_rcode(dns.rcode.NOERROR)
|
||||
yield DnsResponseSend(qctx.response, authoritative=True)
|
||||
|
||||
|
||||
def resend_server() -> AsyncDnsServer:
|
||||
server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
|
||||
server.install_response_handlers(
|
||||
PrimeHandler(),
|
||||
CookieHandler(),
|
||||
NoErrorHandler(),
|
||||
)
|
||||
return server
|
||||
|
||||
|
||||
def main() -> None:
|
||||
resend_server().run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
16
bin/tests/system/resend_loop/ns4/named.conf.j2
Normal file
16
bin/tests/system/resend_loop/ns4/named.conf.j2
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
options {
|
||||
query-source address 10.53.0.4;
|
||||
notify-source 10.53.0.4;
|
||||
transfer-source 10.53.0.4;
|
||||
port @PORT@;
|
||||
pid-file "named.pid";
|
||||
listen-on { 10.53.0.4; };
|
||||
listen-on-v6 { none; };
|
||||
recursion yes;
|
||||
dnssec-validation no;
|
||||
};
|
||||
|
||||
zone "." IN {
|
||||
type hint;
|
||||
file "root.hint";
|
||||
};
|
||||
14
bin/tests/system/resend_loop/ns4/root.hint
Normal file
14
bin/tests/system/resend_loop/ns4/root.hint
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
;
|
||||
; SPDX-License-Identifier: MPL-2.0
|
||||
;
|
||||
; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
; file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
;
|
||||
; See the COPYRIGHT file distributed with this work for additional
|
||||
; information regarding copyright ownership.
|
||||
|
||||
$TTL 999999
|
||||
. IN NS a.root-servers.nil.
|
||||
a.root-servers.nil. IN A 10.53.0.3
|
||||
28
bin/tests/system/resend_loop/tests_resend_loop.py
Normal file
28
bin/tests/system/resend_loop/tests_resend_loop.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# See the COPYRIGHT file distributed with this work for additional
|
||||
# information regarding copyright ownership.
|
||||
|
||||
import dns.message
|
||||
|
||||
import isctest
|
||||
|
||||
|
||||
def test_resend_loop_badcookie(ns4):
|
||||
expected_log = "exceeded max queries resolving 'test.example/A'"
|
||||
|
||||
msg = dns.message.make_query("test.example", "A")
|
||||
with ns4.watch_log_from_here() as watcher:
|
||||
res = isctest.query.udp(msg, ns4.ip)
|
||||
watcher.wait_for_line(expected_log)
|
||||
|
||||
isctest.check.servfail(res)
|
||||
|
||||
prohibited_log = "query failed (timed out) for test.example/IN/A"
|
||||
assert prohibited_log not in ns4.log
|
||||
|
|
@ -4285,6 +4285,39 @@ fctx_nextaddress(fetchctx_t *fctx) {
|
|||
return addrinfo;
|
||||
}
|
||||
|
||||
static isc_result_t
|
||||
incr_query_counters(fetchctx_t *fctx) {
|
||||
isc_result_t result;
|
||||
|
||||
result = isc_counter_increment(fctx->qc);
|
||||
#if WANT_QUERYTRACE
|
||||
FCTXTRACE5("query", "max-recursion-queries, querycount=",
|
||||
isc_counter_used(fctx->qc));
|
||||
#endif
|
||||
if (result != ISC_R_SUCCESS) {
|
||||
isc_log_write(DNS_LOGCATEGORY_RESOLVER, DNS_LOGMODULE_RESOLVER,
|
||||
ISC_LOG_DEBUG(3),
|
||||
"exceeded max queries resolving '%s' "
|
||||
"(max-recursion-queries, querycount=%u)",
|
||||
fctx->info, isc_counter_used(fctx->qc));
|
||||
} else if (fctx->gqc != NULL) {
|
||||
result = isc_counter_increment(fctx->gqc);
|
||||
#if WANT_QUERYTRACE
|
||||
FCTXTRACE5("query", "max-query-count, querycount=",
|
||||
isc_counter_used(fctx->gqc));
|
||||
#endif
|
||||
if (result != ISC_R_SUCCESS) {
|
||||
isc_log_write(DNS_LOGCATEGORY_RESOLVER,
|
||||
DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3),
|
||||
"exceeded global max queries resolving "
|
||||
"'%s' (max-query-count, querycount=%u)",
|
||||
fctx->info, isc_counter_used(fctx->gqc));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void
|
||||
fctx_try(fetchctx_t *fctx, bool retrying) {
|
||||
isc_result_t result;
|
||||
|
|
@ -4425,36 +4458,11 @@ fctx_try(fetchctx_t *fctx, bool retrying) {
|
|||
return;
|
||||
}
|
||||
|
||||
result = isc_counter_increment(fctx->qc);
|
||||
#if WANT_QUERYTRACE
|
||||
FCTXTRACE5("query", "max-recursion-queries, querycount=",
|
||||
isc_counter_used(fctx->qc));
|
||||
#endif
|
||||
result = incr_query_counters(fctx);
|
||||
if (result != ISC_R_SUCCESS) {
|
||||
isc_log_write(DNS_LOGCATEGORY_RESOLVER, DNS_LOGMODULE_RESOLVER,
|
||||
ISC_LOG_DEBUG(3),
|
||||
"exceeded max queries resolving '%s' "
|
||||
"(max-recursion-queries, querycount=%u)",
|
||||
fctx->info, isc_counter_used(fctx->qc));
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (fctx->gqc != NULL) {
|
||||
result = isc_counter_increment(fctx->gqc);
|
||||
#if WANT_QUERYTRACE
|
||||
FCTXTRACE5("query", "max-query-count, querycount=",
|
||||
isc_counter_used(fctx->gqc));
|
||||
#endif
|
||||
if (result != ISC_R_SUCCESS) {
|
||||
isc_log_write(DNS_LOGCATEGORY_RESOLVER,
|
||||
DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3),
|
||||
"exceeded global max queries resolving "
|
||||
"'%s' (max-query-count, querycount=%u)",
|
||||
fctx->info, isc_counter_used(fctx->gqc));
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
result = fctx_query(fctx, addrinfo, fctx->options);
|
||||
if (result != ISC_R_SUCCESS) {
|
||||
goto done;
|
||||
|
|
@ -9459,9 +9467,9 @@ rctx_nextserver(respctx_t *rctx, dns_message_t *message,
|
|||
* rctx_resend():
|
||||
*
|
||||
* Resend the query, probably with the options changed. Calls
|
||||
* fctx_query(), passing rctx->retryopts (which is based on
|
||||
* query->options, but may have been updated since the last time
|
||||
* fctx_query() was called).
|
||||
* fctx_query(), unless query counter limits are hit, passing
|
||||
* rctx->retryopts (which is based on query->options, but may have
|
||||
* been updated since the last time fctx_query() was called).
|
||||
*/
|
||||
static void
|
||||
rctx_resend(respctx_t *rctx, dns_adbaddrinfo_t *addrinfo) {
|
||||
|
|
@ -9469,8 +9477,15 @@ rctx_resend(respctx_t *rctx, dns_adbaddrinfo_t *addrinfo) {
|
|||
isc_result_t result;
|
||||
|
||||
FCTXTRACE("resend");
|
||||
inc_stats(fctx->res, dns_resstatscounter_retry);
|
||||
|
||||
CHECK(incr_query_counters(fctx));
|
||||
|
||||
result = fctx_query(fctx, addrinfo, rctx->retryopts);
|
||||
if (result == ISC_R_SUCCESS) {
|
||||
inc_stats(fctx->res, dns_resstatscounter_retry);
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (result != ISC_R_SUCCESS) {
|
||||
fctx_failure_detach(&rctx->fctx, result);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue