From faf9fb0b824be10322cbe59a62abb1538b7273d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Mon, 18 May 2026 19:27:54 +0200 Subject: [PATCH] Reject malformed RRSIG records A signature cannot cover a meta-type (NONE, ANY, AXFR, IXFR, MAILB, MAILA, OPT, TSIG, TKEY); previously such records were cached by the recursive resolver and collided with negative-cache entries on the same owner name, corrupting the QP-trie cache. Assisted-by: Claude:claude-opus-4-7 (cherry picked from commit c28ba9c3c6f4274a3626cd300a2590a1593ab2f6) --- .../system/qpcache_rrsig_any/ans3/ans.py | 63 ++++++++++++++++ .../system/qpcache_rrsig_any/ns2/named.args | 1 + .../qpcache_rrsig_any/ns2/named.conf.j2 | 39 ++++++++++ .../tests_qpcache_rrsig_any.py | 72 +++++++++++++++++++ lib/dns/message.c | 5 +- 5 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 bin/tests/system/qpcache_rrsig_any/ans3/ans.py create mode 100644 bin/tests/system/qpcache_rrsig_any/ns2/named.args create mode 100644 bin/tests/system/qpcache_rrsig_any/ns2/named.conf.j2 create mode 100644 bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py diff --git a/bin/tests/system/qpcache_rrsig_any/ans3/ans.py b/bin/tests/system/qpcache_rrsig_any/ans3/ans.py new file mode 100644 index 0000000000..67993fc94e --- /dev/null +++ b/bin/tests/system/qpcache_rrsig_any/ans3/ans.py @@ -0,0 +1,63 @@ +""" +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. + +For any query, returns a hand-crafted RRSIG whose Type-Covered field +is selected by the leftmost label of QNAME. The label is parsed as a +DNS type via `dns.rdatatype.from_text()`, so the resolver can be +probed with any meta-type by querying e.g. `any.attacker.test.`, +`axfr.attacker.test.`, `tsig.attacker.test.`, etc. +""" + +from collections.abc import AsyncGenerator + +import dns.flags +import dns.rcode +import dns.rdataclass +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + QueryContext, + ResponseHandler, +) + + +class RrsigCoversHandler(ResponseHandler): + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + covers_label = qctx.qname.labels[0].decode("ascii").upper() + covers = dns.rdatatype.from_text(covers_label) + rrset = dns.rrset.from_text( + qctx.qname, + 3600, + dns.rdataclass.IN, + dns.rdatatype.RRSIG, + f"TYPE{int(covers)} 8 2 3600 20300101000000 20200101000000 " + "12345 attacker.test. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + ) + qctx.response.set_rcode(dns.rcode.NOERROR) + qctx.response.flags |= dns.flags.AA + qctx.response.answer.append(rrset) + yield DnsResponseSend(qctx.response) + + +def main() -> None: + server = AsyncDnsServer() + server.install_response_handler(RrsigCoversHandler()) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/qpcache_rrsig_any/ns2/named.args b/bin/tests/system/qpcache_rrsig_any/ns2/named.args new file mode 100644 index 0000000000..a47f606e65 --- /dev/null +++ b/bin/tests/system/qpcache_rrsig_any/ns2/named.args @@ -0,0 +1 @@ +-m record -c named.conf -d 99 -D qpcache_rrsig_any-ns2 -g diff --git a/bin/tests/system/qpcache_rrsig_any/ns2/named.conf.j2 b/bin/tests/system/qpcache_rrsig_any/ns2/named.conf.j2 new file mode 100644 index 0000000000..98e02cb2c9 --- /dev/null +++ b/bin/tests/system/qpcache_rrsig_any/ns2/named.conf.j2 @@ -0,0 +1,39 @@ +/* + * 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. + */ + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +options { + query-source address 10.53.0.2; + notify-source 10.53.0.2; + transfer-source 10.53.0.2; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.2; }; + listen-on-v6 { none; }; + recursion yes; + dnssec-validation no; +}; + +zone "attacker.test" { + type forward; + forward only; + forwarders { 10.53.0.3 port @PORT@; }; +}; diff --git a/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py b/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py new file mode 100644 index 0000000000..8601eb4346 --- /dev/null +++ b/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py @@ -0,0 +1,72 @@ +#!/usr/bin/python3 + +# 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. + +""" +A signature cannot cover a DNS meta-type. An RRSIG whose Type-Covered +field is one of NONE/ANY/AXFR/IXFR/MAILA/MAILB/OPT/TSIG/TKEY is +malformed and must be rejected by the resolver. ns3 picks the +Type-Covered field from the leftmost label of QNAME. +""" + +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "ans*/ans.run", + "ns*/named.run", + ] +) + + +META_TYPES = ["ANY", "AXFR", "IXFR", "MAILA", "MAILB", "OPT", "TSIG", "TKEY"] + + +@pytest.mark.parametrize("meta_type", META_TYPES) +def test_rrsig_covers_metatype_is_servfail(meta_type): + qname = f"{meta_type.lower()}.attacker.test." + msg = isctest.query.create(qname, "RRSIG", dnssec=False, ad=False) + res = isctest.query.tcp(msg, "10.53.0.2") + isctest.check.servfail(res) + + +@pytest.mark.parametrize("meta_type", META_TYPES) +def test_dig_nobesteffort_rejects_malformed_rrsig(meta_type, named_port): + """ + With +nobesteffort, dig uses the same strict parser path that the + recursive resolver uses, so a malformed RRSIG covering a meta-type + is rejected before being printed. + """ + dig = isctest.run.EnvCmd("DIG", f"-p {named_port}") + qname = f"{meta_type.lower()}.attacker.test." + res = dig( + f"+nobesteffort +tries=1 +time=5 @10.53.0.3 {qname} RRSIG", + raise_on_exception=False, + ) + assert ";; Got bad packet: FORMERR" in res.out + assert "ANSWER SECTION" not in res.out + + +@pytest.mark.parametrize("meta_type", META_TYPES) +def test_dig_besteffort_shows_malformed_rrsig(meta_type, named_port): + """ + The default dig parser runs in +besteffort mode, which intentionally + keeps wire-level inspection working: the malformed RRSIG is still + printed so operators can debug what an upstream actually sent. + """ + dig = isctest.run.EnvCmd("DIG", f"-p {named_port}") + qname = f"{meta_type.lower()}.attacker.test." + res = dig(f"+tries=1 +time=5 @10.53.0.3 {qname} RRSIG") + assert "ANSWER SECTION" in res.out + assert "RRSIG" in res.out diff --git a/lib/dns/message.c b/lib/dns/message.c index dce9c2d65f..91fb29bdd4 100644 --- a/lib/dns/message.c +++ b/lib/dns/message.c @@ -1415,7 +1415,10 @@ getsection(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx, rdata->rdclass = rdclass; if (rdtype == dns_rdatatype_rrsig && rdata->flags == 0) { covers = dns_rdata_covers(rdata); - if (covers == 0) { + /* A signature can only cover a real rdata type */ + if (covers == dns_rdatatype_none || + dns_rdatatype_ismeta(covers)) + { DO_ERROR(DNS_R_FORMERR); } } else if (rdtype == dns_rdatatype_sig /* SIG(0) */ &&