From 1645b09341806e08cc7c997c0d0f20a99db17851 Mon Sep 17 00:00:00 2001 From: Aram Sargsyan Date: Mon, 18 May 2026 09:14:27 +0000 Subject: [PATCH 1/2] Add a new 'deny-answer-aliases' check in 'resolver' system test This new check exercises an attack against guarantees given by the 'deny-answer-aliases' configuration option by caching a DNAME that is a parent of the restricted alias, and then "constructing" the restricted alias from the cache. --- bin/tests/system/resolver/ans2/ans.py | 1 + bin/tests/system/resolver/ans3/ans.py | 30 +++++++++++++- bin/tests/system/resolver/tests_resolver.py | 45 +++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/bin/tests/system/resolver/ans2/ans.py b/bin/tests/system/resolver/ans2/ans.py index 9d27310527..78a94b5e89 100644 --- a/bin/tests/system/resolver/ans2/ans.py +++ b/bin/tests/system/resolver/ans2/ans.py @@ -141,6 +141,7 @@ class Ns2Delegation(DelegationHandler): class Ns3Delegation(DelegationHandler): domains = [ "example.net.", + "isc.org.", "lame.example.org.", "sub.example.org.", ] diff --git a/bin/tests/system/resolver/ans3/ans.py b/bin/tests/system/resolver/ans3/ans.py index 8913186a70..284f43e584 100644 --- a/bin/tests/system/resolver/ans3/ans.py +++ b/bin/tests/system/resolver/ans3/ans.py @@ -41,13 +41,18 @@ from ..resolver_ans import ( ) -class ApexNSHandler(QnameHandler, StaticResponseHandler): +class ApexNSHandler(QnameQtypeHandler, StaticResponseHandler): qnames = ["example.net."] qtypes = [dns.rdatatype.NS] answer = [rrset(qnames[0], dns.rdatatype.NS, f"ns.{qnames[0]}")] additional = [rrset(f"ns.{qnames[0]}", dns.rdatatype.A, "10.53.0.3")] +class AttackDnameHandler(QnameHandler, StaticResponseHandler): + qnames = ["www.example.attack.example.net", "isc.attack.example.net."] + answer = [rrset("attack.example.net.", dns.rdatatype.DNAME, "org.")] + + class BadCnameHandler(QnameHandler, StaticResponseHandler): qnames = ["badcname.example.net."] answer = [rrset(qnames[0], dns.rdatatype.CNAME, "badcname.example.org.")] @@ -64,6 +69,12 @@ class CnameSubHandler(QnameHandler, StaticResponseHandler): answer = [rrset(qnames[0], dns.rdatatype.CNAME, "ok.sub.example.org.")] +class ExampleOrgHandler(QnameQtypeHandler, StaticResponseHandler): + qnames = ["example.org."] + qtypes = [dns.rdatatype.A] + answer = [rrset(qnames[0], qtypes[0], "1.2.3.4")] + + class FooBadDnameHandler(QnameHandler, StaticResponseHandler): qnames = ["foo.baddname.example.net."] answer = [ @@ -93,12 +104,18 @@ class GoodCnameHandler(QnameHandler, StaticResponseHandler): answer = [rrset(qnames[0], dns.rdatatype.CNAME, "goodcname.example.org.")] +class IscHandler(QnameQtypeHandler, StaticResponseHandler): + qnames = ["isc.org."] + qtypes = [dns.rdatatype.A] + answer = [rrset(qnames[0], qtypes[0], "1.2.3.4")] + + class LameExampleOrgDelegation(DelegationHandler): domains = ["lame.example.org."] server_number = 3 -class LargeReferralHandler(QnameHandler, StaticResponseHandler): +class LargeReferralHandler(QnameQtypeHandler, StaticResponseHandler): qnames = ["large-referral.example.net."] qtypes = [dns.rdatatype.NS] authority = [ @@ -172,6 +189,11 @@ class WwwDnameSubHandler(QnameHandler, StaticResponseHandler): ] +class WwwGoodDnameHandler(QnameHandler, StaticResponseHandler): + qnames = ["www.example.gooddname.example.net"] + answer = [rrset("gooddname.example.net.", dns.rdatatype.DNAME, "org.")] + + class WwwHandler(QnameHandler): qnames = ["www.example.net."] @@ -195,9 +217,11 @@ def main() -> None: server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) server.install_response_handlers( ApexNSHandler(), + AttackDnameHandler(), BadCnameHandler(), BadGoodDnameNsHandler(), CnameSubHandler(), + ExampleOrgHandler(), FooBadDnameHandler(), FooBarSubTld1Handler(), FooGoodDnameHandler(), @@ -207,6 +231,7 @@ def main() -> None: Gl6412Ns2Handler(), Gl6412Ns3Handler(), GoodCnameHandler(), + IscHandler(), LameExampleOrgDelegation(), LargeReferralHandler(), LongCnameHandler(), @@ -217,6 +242,7 @@ def main() -> None: OkSubHandler(), PartialFormerrHandler(), WwwDnameSubHandler(), + WwwGoodDnameHandler(), WwwHandler(), ) diff --git a/bin/tests/system/resolver/tests_resolver.py b/bin/tests/system/resolver/tests_resolver.py index a7d1cdef43..1d30e4761b 100644 --- a/bin/tests/system/resolver/tests_resolver.py +++ b/bin/tests/system/resolver/tests_resolver.py @@ -11,6 +11,8 @@ import time +import dns.message + import isctest @@ -40,3 +42,46 @@ def test_resolver_cache_reloadfails(ns1, templates): # The ttl being lower than 300 (provided by fake authoritative) proves # the cache is still in use assert res.answer[0].ttl < 300 + + +# GL#5930 +def test_resolver_dname_target_filter_attack(): + # Control check - this should return 'attack.example.net. DNAME org.', + # which then should result in resolving 'www.example.org. AAAA', which + # should be SERVAIL because example.org is in 'deny-answer-aliases'. + msg = isctest.query.create("www.example.attack.example.net.", "AAAA") + res = isctest.query.udp(msg, "10.53.0.1") + isctest.check.servfail(res) + + # Execute the attack - this should return 'attack.example.net. DNAME org.', + # which then should result in resolving isc.org and caching the DNAME. + msg = isctest.query.create("isc.attack.example.net.", "A") + res = isctest.query.udp(msg, "10.53.0.1") + answer = """;ANSWER +attack.example.net. 300 IN DNAME org. +isc.attack.example.net. 300 IN CNAME isc.org. +isc.org. 300 IN A 1.2.3.4 +;AUTHORITY +;ADDITIONAL +""" + expected_answer = dns.message.from_text(answer) + isctest.check.noerror(res) + isctest.check.rrsets_equal(res.answer, expected_answer.answer) + isctest.check.rrsets_equal(res.authority, expected_answer.authority) + isctest.check.rrsets_equal(res.additional, expected_answer.additional) + + # Vulnerability check - this should return 'attack.example.net. DNAME org.' + # which then should result in resolving 'www.example.org. A', which + # should still be SERVAIL because example.org is in 'deny-answer-aliases', + # unless the attack on the previous step was successful. + msg = isctest.query.create("www.example.attack.example.net.", "A") + res = isctest.query.udp(msg, "10.53.0.1") + isctest.check.servfail(res) + + # Exception check - this should return 'gooddname.example.net. DNAME org.' + # which then should result in resolving 'www.example.org. A', which + # should be NOERROR because while example.org is in 'deny-answer-aliases', + # gooddname.example.net is in the exceptions list. + msg = isctest.query.create("www.example.gooddname.example.net.", "A") + res = isctest.query.udp(msg, "10.53.0.1") + isctest.check.noerror(res) From 45c1d760a89d6d680c33935a53d718d1ecbd0e43 Mon Sep 17 00:00:00 2001 From: Aram Sargsyan Date: Mon, 18 May 2026 09:17:51 +0000 Subject: [PATCH 2/2] Fix a 'deny-answer-aliases' bug when using a cached DNAME When using a cached DNAME to resolve a name, make sure to consult the denied answers lists, otherwise it is possible to consutruct a restricted alias by caching a DNAME that is a parent of the denied alias. See the comments in the tests case from the previous commit an example. --- lib/ns/query.c | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/ns/query.c b/lib/ns/query.c index ca76df4f3d..4fd70e6fbc 100644 --- a/lib/ns/query.c +++ b/lib/ns/query.c @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -10137,6 +10138,7 @@ query_cname(query_ctx_t *qctx) { dns_name_copy(&cname.cname, tname); dns_rdata_freestruct(&cname); + ns_client_qnamereplace(qctx->client, tname); qctx->want_restart = true; if (!WANTRECURSION(qctx->client)) { @@ -10260,6 +10262,37 @@ query_dname(query_ctx_t *qctx) { goto cleanup; } + /* + * If the target is a denied alias, and both the `except-from` list + * and the subdomain rule of the `deny-answer-aliases` + * configuration option (see ARM) don't give an exception, then + * answer with a SERVFAIL. + */ + dns_fixedname_t fdeniedname; + dns_name_t *deniedname = dns_fixedname_initname(&fdeniedname); + if (qctx->view->denyanswernames != NULL && + dns_nametree_covered(qctx->view->denyanswernames, qctx->fname, + deniedname, 0) && + !dns_nametree_covered(qctx->view->answernames_exclude, + qctx->client->query.qname, NULL, 0) && + !dns_name_issubdomain(qctx->client->query.qname, deniedname)) + { + char qnamebuf[DNS_NAME_FORMATSIZE]; + char tnamebuf[DNS_NAME_FORMATSIZE]; + + dns_name_format(qctx->client->query.qname, qnamebuf, + sizeof(qnamebuf)); + dns_name_format(qctx->fname, tnamebuf, sizeof(tnamebuf)); + ns_client_log(qctx->client, NS_LOGCATEGORY_QUERIES, + NS_LOGMODULE_QUERY, ISC_LOG_NOTICE, + "DNAME target %s denied for %s (cache)", tnamebuf, + qnamebuf); + QUERY_ERROR(qctx, DNS_R_SERVFAIL); + ns_client_releasename(qctx->client, &qctx->fname); + (void)ns_query_done(qctx); + goto cleanup; + } + ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf); /*