mirror of
https://github.com/isc-projects/bind9.git
synced 2026-05-28 04:34:54 -04:00
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 c28ba9c3c6)
This commit is contained in:
parent
edc1ef084f
commit
faf9fb0b82
5 changed files with 179 additions and 1 deletions
63
bin/tests/system/qpcache_rrsig_any/ans3/ans.py
Normal file
63
bin/tests/system/qpcache_rrsig_any/ans3/ans.py
Normal file
|
|
@ -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()
|
||||
1
bin/tests/system/qpcache_rrsig_any/ns2/named.args
Normal file
1
bin/tests/system/qpcache_rrsig_any/ns2/named.args
Normal file
|
|
@ -0,0 +1 @@
|
|||
-m record -c named.conf -d 99 -D qpcache_rrsig_any-ns2 -g
|
||||
39
bin/tests/system/qpcache_rrsig_any/ns2/named.conf.j2
Normal file
39
bin/tests/system/qpcache_rrsig_any/ns2/named.conf.j2
Normal file
|
|
@ -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@; };
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -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) */ &&
|
||||
|
|
|
|||
Loading…
Reference in a new issue