head 1.1; branch 1.1.1; access; symbols bind-2-20-29:1.1.1.2 bind-9-20-27:1.1.1.1 ISC:1.1.1; locks; strict; comment @# @; 1.1 date 2026.08.29.14.32.03; author christos; state Exp; branches 1.1.1.1; next ; commitid 9oWNYFKZGQLDDxTG; 1.1.1.1 date 2026.08.29.14.32.03; author christos; state Exp; branches; next 1.1.1.2; commitid 9oWNYFKZGQLDDxTG; 1.1.1.2 date 2026.09.17.17.45.02; author christos; state Exp; branches; next ; commitid 6QdcGjuUmo5e60WG; desc @@ 1.1 log @Initial revision @ text @#!/usr/bin/python3 # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 from collections.abc import AsyncGenerator from dataclasses import dataclass from pathlib import Path import json from cryptography.hazmat.primitives import serialization import dns.dnssec import dns.flags import dns.message import dns.name import dns.rcode import dns.rdata import dns.rdataclass import dns.rdatatype import dns.rrset from isctest.asyncserver import ( AsyncDnsServer, DnsResponseSend, QueryContext, ResponseHandler, ) TTL = 300 PARENT = "p031.test." CHILD = f"c.{PARENT}" GRANDCHILD = f"grand.{CHILD}" GRANDCHILD3 = f"grand3.{CHILD}" ATTACK = f"www-bind.{GRANDCHILD}" ATTACK3 = f"www-bind.{GRANDCHILD3}" FORGED_A = "6.6.6.60" CHILD_DS = "12345 13 2 abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" @@dataclass(frozen=True) class Key: zone: dns.name.Name private_key: object dnskey: dns.rdata.Rdata def name(text: str) -> dns.name.Name: return dns.name.from_text(text) def load_key() -> Key: path = Path(__file__).resolve().parent / "keys.json" with path.open(encoding="utf-8") as keys_file: raw_key = json.load(keys_file)[PARENT] private_key = serialization.load_pem_private_key( raw_key["private_pem"].encode("ascii"), password=None, ) dnskey = dns.rdata.from_text( dns.rdataclass.IN, dns.rdatatype.DNSKEY, raw_key["dnskey"] ) return Key(name(PARENT), private_key, dnskey) def rrset(owner: str, rdtype: dns.rdatatype.RdataType, *rdatas: str) -> dns.rrset.RRset: return dns.rrset.from_text(owner, TTL, dns.rdataclass.IN, rdtype, *rdatas) def rrset_from_rdata(owner: str, rdata: dns.rdata.Rdata) -> dns.rrset.RRset: return dns.rrset.from_rdata(name(owner), TTL, rdata) def add_signed( section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key ) -> None: rrsig = dns.dnssec.sign( covered, signer.private_key, signer.zone, signer.dnskey, lifetime=86400, verify=True, ) section.append(covered) section.append(dns.rrset.from_rdata(covered.name, covered.ttl, rrsig)) def soa_rrset() -> dns.rrset.RRset: return rrset( PARENT, dns.rdatatype.SOA, f"ns.{PARENT} hostmaster.{PARENT} 1 3600 600 86400 300", ) def nsec_rrset(owner: str, next_name: str, *types: str) -> dns.rrset.RRset: return rrset(owner, dns.rdatatype.NSEC, f"{next_name} {' '.join(types)}") def child_ds_rrset() -> dns.rrset.RRset: return rrset(CHILD, dns.rdatatype.DS, CHILD_DS) def grandchild_nsec_lie() -> dns.rrset.RRset: return nsec_rrset(GRANDCHILD, f"grandz.{CHILD}", "NS", "RRSIG", "NSEC") def grandchild3_nsec3_lie() -> dns.rrset.RRset: # An NSEC3 owned by the grandparent zone P that matches the hash of the # grandchild name and shows an (insecure) delegation: NS bit set, DS bit # clear. Same forgery as grandchild_nsec_lie(), but expressed as NSEC3 so # that the resolver reaches is_insecure_referral()'s trynsec3 arm. digest = dns.dnssec.nsec3_hash(name(GRANDCHILD3), None, 0, 1).lower() owner = f"{digest}.{PARENT}" return rrset(owner, dns.rdatatype.NSEC3, f"1 0 0 - {digest} NS") def add_parent_nodata( response: dns.message.Message, parent_key: Key, nsec: dns.rrset.RRset ) -> None: add_signed(response.authority, soa_rrset(), parent_key) add_signed(response.authority, nsec, parent_key) def prepare_response(qctx: QueryContext) -> dns.message.Message: qctx.prepare_new_response(with_zone_data=False) qctx.response.flags |= dns.flags.AA qctx.response.set_rcode(dns.rcode.NOERROR) return qctx.response class GrandparentNsecHandler(ResponseHandler): def __init__(self, parent_key: Key) -> None: self.parent_key = parent_key self.parent = name(PARENT) self.child = name(CHILD) self.grandchild = name(GRANDCHILD) self.grandchild3 = name(GRANDCHILD3) def match(self, qctx: QueryContext) -> bool: return qctx.qname.is_subdomain(self.parent) async def get_responses( self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: response = prepare_response(qctx) if qctx.qname == self.parent and qctx.qtype == dns.rdatatype.DNSKEY: # Priming, parent DNSKEY add_signed( response.answer, rrset_from_rdata(PARENT, self.parent_key.dnskey), self.parent_key, ) elif qctx.qname == self.parent and qctx.qtype == dns.rdatatype.SOA: # Priming, parent SOA add_signed(response.answer, soa_rrset(), self.parent_key) elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.DS: # Priming, child DS add_signed(response.answer, child_ds_rrset(), self.parent_key) elif qctx.qname == self.grandchild and qctx.qtype == dns.rdatatype.DS: # Forge no data for grand child DS (NSEC variant) add_parent_nodata(response, self.parent_key, grandchild_nsec_lie()) elif qctx.qname == self.grandchild3 and qctx.qtype == dns.rdatatype.DS: # Forge no data for grand child DS (NSEC3 variant) add_parent_nodata(response, self.parent_key, grandchild3_nsec3_lie()) elif ( qctx.qname.is_subdomain(self.grandchild) or qctx.qname.is_subdomain(self.grandchild3) ) and qctx.qtype == dns.rdatatype.A: # Attack query response.answer.append( rrset(qctx.qname.to_text(), dns.rdatatype.A, FORGED_A) ) else: response.set_rcode(dns.rcode.NXDOMAIN) yield DnsResponseSend(response, authoritative=True) def main() -> None: server = AsyncDnsServer(default_aa=True) server.install_response_handlers(GrandparentNsecHandler(load_key())) server.run() if __name__ == "__main__": main() @ 1.1.1.1 log @Import bind-9-20-27 (Previous was bind-9-20-24) iNotes for BIND 9.20.27# New Features# Disclose active Negative Trust Anchors with Extended DNS Error 33. A Negative Trust Anchor (RFC 7646) turns off DNSSEC validation for a domain, so a name that would normally fail validation resolves instead. named now marks such answers with Extended DNS Error code 33, "Negative Trust Anchor", so operators can see at a glance when a response came back only because an NTA was in effect. [GL #6268] Feature Changes# Speed up RPZ policy zone updates. RPZ updates used to be applied one small step at a time, adding overhead on large policy zones. Updates are now applied as a single batch, improving update performance for large RPZ zones, at the cost of no longer overlapping with concurrent updates. [GL #5787] [GL #6270] Bug Fixes# Ensure NSEC authority does not cross zonecut boundary. When using a cached NSEC record to prove that a delegation is insecure, named now checks that the signer name in the corresponding RRSIG is not above a known secure delegation point. This prevents a signed namespace from being downgraded to insecure using an NSEC record from the grandparent zone. [GL #5967] Treat an unusable NSEC3 chain as a verification failure. When transferring in a mirror zone, DNSSEC verification could incorrectly succeed when the zone had an invalid NSEC3PARAM record, leading to subsequent validation failures. This has been fixed. [GL #6136] Treat non-canonical RPZ prefixes as any other failure. RPZ prefixes that were not encoded in canonical form did not work. They are now handled in the same way as any other encoding error. [GL #6043] Negative caching stopped working with stale-answer-client-timeout set to 0. Negative answers were re-fetched on every query instead of once they actually expired, effectively disabling negative caching. This has been fixed. [GL #6245] An unterminated OpenSSL private-key Label: field could be read past its parser buffer. The Label: field in a .private key file is now checked for length and NUL-termination. Malformed files are rejected. [GL #6193] Restore SMF support on Solaris and illumos. [GL #6096] Fix compilation on GNU/Hurd. [GL #6285] dig +yaml was producing invalid YAML when a lookup failed. When no server could be reached, dig printed its plain-text startup banner ahead of the YAML output, making the result unparsable. dig no longer does this and correctly reflects options such as +nocmd, +short and +yaml, regardless of where they appear on the command line. [GL #1230] Properly prevent TSIG generation command line injection attacks. When key names are generated with rndc-confgen, tsig-keygen and ddns-confgen, special characters must be escaped to ensure that the configuration is parsed correctly. [GL #6071] Fix a potential heap bounds overflow write in dnssec-signzone. It was possible for dnssec-signzone to overflow array bounds while signing. This has been fixed. [GL #6076] Fix crashes on invalid DNSTAP input in dnstap-read. Malformed DNSTAP files could trigger a NULL pointer dereference or an out-of-bounds memory read in dnstap-read. This has been fixed. [GL #6077] [GL #6124] Notes for BIND 9.20.26# Security Fixes# Correct verification of NSEC3 signer name. (CVE-2026-10723) Previously, named accepted child-zone NSEC3 records where the first label equaled the hash of the parent zone as valid parent-zone closest encloser proofs. This has been fixed. ISC would like to thank Qifan Zhang of Palo Alto Networks for bringing this vulnerability to our attention. [GL #5874] Malformed DNSKEY records could trigger an assertion. (CVE-2026-10822) Previously, dns_name_fromwire() did not honor the record boundary when reading names from the wire, allowing malformed records to be accepted when they should not have been. In particular, malformed DNSKEY records could trigger an assertion failure when being printed. This has been fixed. [GL #6004] Fix handling of RPZ CNAME expansion that returns too-long name. (CVE-2026-11331) Previously, if the expansion of a wildcard CNAME RPZ policy resulted in a name that exceeded the length limit, a self-referential CNAME and the original address record were returned, allowing the policy to be bypassed. In branches up to 9.20 this also left query processing in an inconsistent state, which could trigger an assertion failure. named now returns a YXDOMAIN response, without the address. ISC would like to thank Laith Mash'al (0xmshal) for bringing this vulnerability to our attention. [GL #5856] Prevent excessive validation work from crafted negative responses. (CVE-2026-11605) Previously, a validating resolver could be made to perform a large amount of DNSSEC validation work in response to a single answer, consuming excessive CPU. A malicious authoritative server could trigger this by returning a signed negative answer (NXDOMAIN or NODATA) padded with many denial-of-existence proof records, which the resolver continued to verify beyond its per-query validation limit. It now enforces that limit on negative answers and returns SERVFAIL once the limit is reached. [GL #4463] Prevent cache exhaustion under sustained attack. (CVE-2026-11622) Cache memory could become exhausted with expired entries whose memory was not released, due to a sustained attack on the same DNS name that prevented the cleanup. This has been fixed. [GL #4760] Stop accepting invalid signed wildcard records. (CVE-2026-11721) Signed wildcard responses in which the Labels field in the RRSIG record was less than the number of labels in the Signer Name field were being incorrectly accepted. This in turn broke synth-from-dnssec, which depends on such records being correctly validated. This has been fixed. ISC would like to thank Qifan Zhang of Palo Alto Networks for bringing this vulnerability to our attention. [GL #5871] Do not assert for some specific CNAME and DNAME queries. (CVE-2026-12617) A bug in the resolver's handling of certain cached DNAME and CNAME responses could cause named to trigger an assertion failure and exit. An attacker controlling a domain name and the authoritative DNS server it was hosted on could exploit this behavior to cause a denial-of-service. This has been fixed. ISC would like to thank Qifan Zhang of Palo Alto Networks for bringing this vulnerability to our attention. [GL #5946] Prevent crash from malformed NSEC/NSEC3 response. (CVE-2026-13204) An assertion could be triggered by an improperly signed NOQNAME proof. This has been fixed. ISC would like to thank Qifan Zhang of Palo Alto Networks for bringing this vulnerability to our attention. [GL #5985] Fix DNSSEC validation bypass via out-of-zone NSEC Next Field. (CVE-2026-13321) Previously, a malicious zone with out-of-zone NSEC next-owner names could cause a DNSSEC-validating resolver to cache such a record and, if synth-from-dnssec was enabled, to generate negative answers for any zone that was covered by the range. This has been fixed. ISC would like to thank Qifan Zhang of Palo Alto Networks for bringing this vulnerability to our attention. [GL #5873] Reclaim memory promptly when DNSSEC validations are canceled. When a resolver is flooded with queries that require DNSSEC validation - for example during a random-subdomain attack - many of those validations are canceled before they complete. Previously, a canceled validation still kept its place in the internal work queue and held the associated response in memory until that queued work eventually ran, so memory could climb sharply under sustained load. The internal work queue is now dropped as soon as the validation is canceled, releasing the memory it was holding. [GL #4760] Removed Features# Remove the secondary validator in query.c. Previously, when the additional section of a response was being populated, if cached data was found with pending trust, it would be opportunistically validated. The code implementing this validation was not quite formally correct. Rather than fixing it, the code has been removed: RRsets with pending trust are now omitted from responses. [GL #5966] [GL #5968] [GL #5972] Bug Fixes# Fix a bug in DNS UPDATE processing with inline-signing enabled. In rare cases the named process could terminate unexpectedly when processing authorized DNS UPDATE messages in quick succession that were updating a zone with inline-signing enabled. This has been fixed. [GL #5816] Properly detect private records before copying. Previously, an assertion was triggered when trying to copy a private record to a buffer for modification. named now extends the private type detection and copies the contents after rejecting invalid private records. [GL #5857] Tighten referral DS acceptance. Previously, named accepted DS records for sibling zones when it shouldn't have. This has been fixed. [GL #5870] Don't synthesize negative responses with pending NSEC. If an NSEC record has not yet been validated and is cached with trust pending, named no longer uses it to synthesize negative responses. [GL #5872] [GL #5887] [GL #5977] Check that an NSEC signer is at or above the name to be validated. A check has been added to ensure that an NSEC record being used as a proof of nonexistence for a given name is not signed by a name lower in the DNS hierarchy than the one in question. [GL #5876] Don't evict DNSSEC-validated cache data on a CD=1 NXDOMAIN. When a client sent a query with the checking-disabled (CD) bit set and the answer was NXDOMAIN, the resolver cached that unvalidated negative response and discarded any DNSSEC-validated records it already held for the same name, even though the validated data was more trustworthy. A single such response - including a forged one - could flush validated records from the cache and force the resolver to fetch them again. The resolver now checks the trust level of the existing data first and leaves the cache unchanged when it is already validated. [GL #5877] Fix a deny-answer-aliases configuration bypass issue. It was possible to use a maliciously crafted authoritative zone to make a named resolver synthesize a DNAME "alias" that should have been rejected by the configured deny-answer-aliases option. This has been fixed. [GL #5930] Reject external referrals from forwarders. Under a forward first; policy in a forwarding zone, named could accept NS records above the forward zone apex from negative responses. This has been fixed. [GL #5937] Fix a zone transfer over TLS (XoT) issue when using the opportunistic TLS mode. The named process, running as a secondary DNS server and configured to transfer a zone from a primary server using an encrypted XoT transport in opportunistic TLS mode (i.e. without peer certificate/hostname validation), could terminate unexpectedly when the TLS ALPN negotiation with the primary server was unsuccessful. This has been fixed. [GL #5957] Unvalidated opt-out NSEC3 could be accepted in insecurity proof. When determining whether an insecure delegation was legitimate, NSEC3 opt-out records which had not yet passed validation could be used. This has been fixed. [GL #5970] Check wildcard signer and NOQNAME signer match. A positive wildcard answer, and the NSEC3 proof that the requested name doesn't exist in the zone, must both be from the same zone. Otherwise, an NSEC3 from an ancestor zone could be used to interfere with validation. named now retrieves the signer name from a wildcard response's signature. An NSEC3 record cannot be used as a NOQNAME proof for the wildcard unless it exactly matches the name one level above the NSEC3. [GL #5971] Fix CNAME resolution failure caused by a cached SERVFAIL response. Under certain circumstances, a cached SERVFAIL response could incorrectly prevent successful resolution of a CNAME target. This could cause resolution failures to persist until the cached SERVFAIL entry expired, even when the CNAME target itself was otherwise resolvable. This has been fixed. [GL #5983] Reject unsupported RSA DNSKEY shapes during DNSSEC validation. An authoritative server publishing an RSA DNSKEY with an unusually large modulus or an exotic public exponent could make each DNSSEC signature check on a validating recursive resolver noticeably more expensive than for a normally sized key. Such DNSKEYs are now treated as invalid. [GL #6008] Fix a bug in GeoIP2 string matching. When using GeoIP2 ACLs (see acl), named could incorrectly match a name using a sub-string instead of the full name match. This has been fixed. [GL #6019] Fix DNS-over-HTTPS (DoH) quota configuration issue. The http-listener-clients and http-streams-per-connection configuration options could be truncated to smaller values (or to 0, which means unlimited) when very large configuration values in excess of 65535 were used. It is very unlikely that such large values were used in production, and the default values for the affected options are 300 and 100, respectively. This has been fixed. [GL #6021] Truncated reply to a TSIG query no longer stalls the resolver. When an upstream server returned a truncated reply to a query that named had signed with TSIG, the resolver could keep waiting for a follow-up UDP packet that never arrived, stalling the query until it hit the resolver-query-timeout and the client received no answer. named now treats any reply it cannot authenticate as an immediate failure and returns SERVFAIL right away as a defense in depth. [GL #6028] Ignore updates removing DNSKEY RRset with class ANY. When a dynamic update is received that removes the DNSKEY (or CDNSKEY, or CDS) RRset, named now removes all records except the ones that are in use for signing the zone. [GL #6045] Ignore 0-byte reads in the TCP read callback. Callbacks for libuv stream reads do not signal zero-length reads as a failure signal but rather as EAGAIN/EWOULDBLOCK. This could trigger an assertion when a zero-length read was pushed onto a PROXYv2 endpoint that had not yet processed the headers, as it expected a non-NULL region of positive length. [GL #6140] Only print per-zone glue stats when zone-statistics is set to full. The code printing query statistics was ignoring the zone-statistics option. This has been fixed. [GL #6164] CDS/CDNSKEY records were not removed when re-configuring the server. When on an rndc reconfig the DNSSEC policy changes such that it changes the expected CDNSKEY and/or CDS records in the zone, the RRset should be updated accordingly. This did not happen when removing digests from the configuration, or setting cdnskey no;. This has been fixed. [GL #6166] Fix a crash when querying an empty non-terminal in a wildcard zone in RBTDB. A query for an empty non-terminal in a wildcard zone served from the RBT zone database could abort named with an assertion failure. It now returns the correct NODATA answer. [GL #6170] Stop reusing outgoing TCP connections the peer has already closed. Previously, named could hand a new query to an idle forwarder/upstream TCP or TLS connection that the peer had already closed, causing the query to fail (and CLOSE-WAIT sockets to pile up). Idle reused connections are now watched, so a close is noticed and the connection is dropped instead of reused. A new tcp-reuse-timeout option controls how long an idle outgoing connection is kept open for reuse (default 5 seconds). [GL #6171] Fix DNSSEC validation failures for names under an apex DNAME. DNSSEC validation could fail with SERVFAIL for names covered by a DNAME at the apex of a signed zone, unless the zone's keys were already validated in the cache. This regression was introduced by the recent fix for resolver stalls on CNAME responses to DS queries, and has now been addressed. [GL #6176] Notes for BIND 9.20.25# Note The BIND 9.20.25 release was withdrawn after the discovery of a regression in a security fix in it during pre-release testing. @ text @@ 1.1.1.2 log @Import bind-9.20.29 (previous was 9.20.27) BIND 9.20.29 Security Fixes [CVE-2026-19668] Prevent excessive CPU use validating crafted DNSSEC responses. a0a61dba9e A malicious authoritative server could serve a securely delegated zone whose DS and DNSKEY records carry many distinct key tags but no valid match, forcing a validating resolver into excessive key-tag matching and high CPU use for every query. BIND now bounds this work with the per-query validation limit (max-validations-per-fetch). [GL #5349] [CVE-2026-19033] Require a TSIG on every message of incoming zone transfers. 9404cd2b8c BIND 9 used to accept TSIG-signed zone transfers in which some messages were unsigned, and processed those messages before the next signature could vouch for them. It now requires a TSIG on every message of an incoming AXFR or IXFR; all modern nameserver already sign every message, so no change is expected in practice. [GL #6062] [CVE-2026-77119] Prevent a DNSSEC downgrade of secure delegations via unrelated NSEC3. 3bed9c8e9e A validating resolver could be tricked into treating a secure delegation as unsigned and accepting forged answers for names beneath it, if an attacker could inject responses to its queries. Such forged proofs are now rejected. [GL #6234] [CVE-2026-19941] Prevent forged DNSSEC-validated NXDOMAIN responses. a36bf58daf A validating resolver could accept a signed NSEC record from an unrelated zone as proof that a wildcard did not exist. An on-path attacker or malicious forwarder controlling a signed zone could therefore forge an authenticated NXDOMAIN response for a name that should resolve through a wildcard. BIND now requires the wildcard-denial and name-nonexistence proofs to be signed by the same zone. [GL #6253] [CVE-2026-19666] DNS64 with break-dnssec could cause an assertion failure. 4cec4965c4 When a "dns64" statement is configured with "break-dnssec yes" and its "exclude" list matches some but not all of the addresses in an AAAA RRset, named removes the excluded addresses from the answer instead of synthesizing new ones. If the answer being filtered had been cached together with a proof that the queried name does not exist -- which is what a wildcard match produces -- named terminated with an assertion failure. Only recursive resolvers are affected, and only when "break-dnssec yes" is in use; the answer has to come from the cache, so a server that is only authoritative cannot reach this. [GL #6301] [CVE-2026-19667] Reject negative cache records that do not fit in a dns_rdata_t. dbf08c8581 A single crafted response from a server could make a resolver cache a malformed negative entry and then terminate with an assertion failure when reading it back. Only recursive resolvers are affected, on a default configuration. [GL #6302] [CVE-2026-19662] Prevent resolver crash with cached DNSSEC proofs. c884cc1ba0 Under certain timing conditions, concurrent recursive queries could cause named to crash when cached DNSSEC NOQNAME proof data was replaced while still in use. Cached proof data is now retained until all queries using it have completed. [GL #6333] [CVE-2026-75029] Discard repeated SOA, CNAME, and DNAME records when parsing DNS messages. 0d630758c2 A DNS message could carry the same SOA, CNAME, or DNAME record many times, and named kept every copy while parsing it. With name compression those copies took up far more memory internally than in the message itself, and every later processing step had to handle all of them. named now keeps the first copy of such a record and discards identical repeats. [GL #6335] [CVE-2026-77692] Fix an unauthenticated crash on HTTPS using SIG(0) 5a24401c5c A specifically crafted HTTPS query using SIG(0) as authentication could crash named if the client closes the connection before named actually verifies the signature. This is now fixed. [GL #6343] [CVE-2026-81736] Cached HTTPS/SVCB aliases could exhaust resolver CPU. 20bbb1639a A recursive resolver that had cached a large set of interlinked HTTPS or SVCB records in alias form could be driven to do an excessive amount of work assembling a single response, because it followed every cached alias target when building the additional section. A client permitted to use recursion, together with an attacker-controlled zone used to plant the records, could repeat small queries to consume enough CPU to delay or deny service to other clients. The amount of additional processing done for one query is now bounded. [GL #6347] [CVE-2026-76163] Prevent TKEY queries from terminating named without global options. 7645138538 named could terminate unexpectedly when a remote client sent a TKEY query if the configuration did not include a global options statement. This has been fixed. ISC thanks Owais Lone (thesecguy) for reporting the issue. [GL #6357] [CVE-2026-78301] Out-of-zone records in a zone database could be served as authoritative. 72a10c3a0b When a zone database contained records for names outside the zone -- such as a delegation above the zone apex, left behind by a secondary that had accepted out-of-zone data from its primary -- the server could treat them as authoritative and answer queries for names inside the zone with that out-of-zone data instead of the zone's own. A server that was also a resolver could follow such a delegation and cache the answers of the server it named, affecting names outside the configured zone. Zone database lookups are now confined to names at or below the zone's origin. ISC would like to thank Henrique Pereira for reporting the issue. [GL #6361] [CVE-2026-80274] Crash on wildcard answers carrying both NSEC and NSEC3 proofs. 0e44451b1a When a wildcard answer arrived with both NSEC and NSEC3 records at the name proving that the queried name does not exist, the resolver could pick different records when caching the answer and when retrieving the proof, depending on the order in which the authoritative server sent them. This could terminate named with an assertion failure, fail the query with SERVFAIL, or serve a denial record other than the one that had been verified. The resolver now caches and serves the same denial record it accepted when the answer was received. ISC would like to thank hythyt for reporting the issue. [GL #6369] [CVE-2026-81563] Following HTTPS/SVCB aliases could leak resolver cache memory. 3162df369e When a recursive server answered a query for an HTTPS or SVCB record in alias form and the alias target had more than 13 records, the target records were pinned in the cache permanently instead of being released once the answer was sent. A remote party who could make the server follow such aliases to a steady stream of fresh names could grow the cache beyond the configured max-cache-size until the server was unable to resolve unrelated names. The records are now released correctly. ISC would like to thank Samy Medjahed/Ap4sh for reporting the issue. [GL #6374] New Features Add an agent skill for the isc_job/isc_async/isc_work APIs. fe32990b06 Documents when to use isc_job_run(), isc_async_run() or isc_work_enqueue(), and the contract each one imposes. No functional change. [GL !12561] Removed Features Remove unused closest encloser proof caching. abd8b5bfd8 BIND used to cache an NSEC3 closest encloser proof alongside positive wildcard answers so that a resolver could re-send it when answering from its cache. That stopped being used in BIND 9.9 (2011), when positive wildcard responses were changed to omit that NSEC3 record -- RFC 5155 requires only the next closer name proof -- and the closest encloser came to be derived during validation instead. The caching code has been unreachable ever since, so this removes it with no change in behaviour. [GL #5803] [GL !12660] Feature Changes Reject oversized and malformed DNSKEY records up front. 6c22109924 Oversized RSA key material in a DNSKEY record was only rejected after it had been converted, allocating memory proportional to the record size. Such records are now rejected before conversion, as are Ed25519 and Ed448 keys with trailing bytes that were previously silently ignored. [GL #4537] [GL !12560] Bug Fixes Prevent a crash when using both dns64 and filter-a. bce5d10d18 An assertion failure was possible when using both dns64 and the filter-a plugin simultaneously; this has been fixed. [GL #5979] [GL !12663] Fix update-policy grant external address passing. b1e955c326 Only TCP client addresses are supposed to be passed to an external handler for the associated update-policy rule, but UDP client addresses were also being passed. This could have caused the external handler to return a result it otherwise wouldn't. This has been fixed. [GL #6061] [GL !12555] Missing required NSEC3 for delegation not detected. e84ed2e9d7 A missing required NSEC3 record for an insecure delegation in a non OPTOUT range was not being detected. This has been fixed. [GL #6063] [GL !12611] Tighten EUI48 and EUI48 text parsing. ff50f2cdf1 Malformed EUI48 and EUI64 records could be accepted. This has been fixed. [GL #6082] [GL !12521] GeoIP ACL state can be stale or wrong after reload. 63baf425b3 named caches GeoIP information after looking it up, but the cached information was not invalidated when the GeoIP database was reloaded, so it could continue to be used. We now invalidate existing cached GeoIP information as part of the reloading process. [GL #6083] [GL !12662] Honor DNSSEC policy key tag ranges. b82e5834b7 When a DNSSEC policy configured a non-default tag-range, dnssec-keygen and dnssec-ksr could accept generated keys outside that range. Both tools now honor the configured minimum and maximum key tags. [GL #6091] [GL !12549] Fix double free in mdig when EDNS options are specified. af5bd0b0ff When the default_query is cloned the EDNS options need to be cloned rather than the pointer copied. The old behaviour results in a double free of the options. This has been fixed. [GL #6095] [GL !12661] Fix a crash when an IXFR falls back to AXFR with updates still pending. e34062bc7e When a secondary zone received an incremental transfer (IXFR) and the primary then caused named to fall back to a full transfer (AXFR) while some of the already-received incremental changes were still waiting to be applied, named could later crash when that transfer finished. The pending changes are now discarded correctly before the AXFR retry. [GL #6114] [GL !12624] Fix DS requests to parental agents over TLS. 55830d30f6 TLS configuration for parental agents was being ignored when sending DS requests. This has been fixed. [GL #6135] [GL !12613] Fix a crash when resolving names below a cached DNAME. b94e940f52 A recursive resolver could crash when it answered a query for a name beneath a cached DNAME while that same DNAME record was concurrently refreshed or evicted from the cache. [GL #6182] [GL !12593] Rndc-confgen -q (quiet) option is documented but doesn't work. 7e4a7ca1a7 The command line parsing in rndc-confgen was broken so rndc-confgen -q did not work. This has been fixed. [GL #6187] [GL !12575] Enforce query ACLs for redirect zones and searched DLZs. bc69876b2e Queries answered from redirect zones or searched DLZ databases did not consistently honor allow-query and allow-query-on, potentially exposing restricted DNS data to excluded clients or through excluded listening addresses. These ACLs are now enforced before redirect or DLZ data is returned. [GL #6251], #6252 [GL !12646] Check "asnum" validity in GeoIP ACLs. 28c2bfdc7b We now check the validity of autonomous system (AS) numbers when parsing GeoIP ACLs that use asnum elements at configuration time. asnum values start with an optional case-insensitive "AS" prefix, followed only by decimal digits, with no spaces or other extraneous characters. The value represented cannot exceed 2^32. [GL #6255] [GL !12511] Prevent crashes while reporting DNSSEC signing statistics. c190514f0a Servers with zone-statistics full could terminate while reporting DNSSEC signing statistics for a zone tracking adding more than four signing keys. [GL #6256] [GL !12674] Fix various nits in the netmgr code. c28cdad51b The MR consists of couple of small fixes and uncaught errors in the Network Manager. [GL #6257] [GL !12576] Fix a crash on remote-servers lists that reference themselves. aaae614f9d Since 9.21.16 and 9.20.17, a remote-servers, primaries, masters, or parental-agents list that referenced itself, directly or through another list, made named crash on startup or reconfiguration. Such references are again skipped and the remaining entries in the list are used, as in earlier versions. [GL #6287] [GL !12604] A record from outside a response policy zone could stop named. d135513b37 A response policy zone transferred from a primary can contain a record whose name lies outside the zone. Such a record could stop named, both when it arrived and again at every startup afterwards, because a secondary keeps it in its own copy of the zone. Records like this are now rejected and logged; previously one could also silently create a policy entry for an unrelated name. [GL #6304] [GL !12543] "rndc flushtree ." failed to flush the cache. 96e8b585ed rndc flushtree flushes cache data below a specified name. If the name specified is the DNS root, it should fully empty the cache, the same as rndc flush. However, there was a bug causing the command, in that case, to have no effect on the cache at all; this has been fixed. [GL #6308] [GL !12582] Invalid key-store configuration could abort the DNSSEC tools. 1d796ab072 Invalid configured key-stores named "key-directory" in configuration files could abort the DNSSEC tools. This has been fixed. [GL #6313] [GL !12653] NSEC signature set could bypass the secure-delegation check. c966177f6c When proving that a delegation is insecure, the validator bounded an NSEC record's authority by the signer of whichever RRSIG happened to come first in the record's signature set, rather than the signature that actually verified. A grandparent NSEC padded with an extra, unverifiable signature could therefore pass the check that keeps such proofs from reaching below a signed child zone. The validator now requires every signature on the NSEC to name the same signer and refuses proofs whose signature set is malformed or larger than max-validations-per-fetch allows. [GL #6321] Fix a possible nsupdate issue when using GSS-TSIG. 4ddcab2d3c The nsupdate process could terminate unexpectedly when using the GSS-TSIG mode executed with the nsupdate -g option. This has been fixed. [GL #6325] [GL !12588] Fix isccc_alist_define error paths. af1349552a If there is an out of memory error in isccc_alist_define a memory leak (the sexpr holding the key name) or a double free (value) could occur. This has been fixed. [GL #6329] [GL !12636] Check for empty 'endpoints' list. 23f58af443 Configuring an http block with endpoints {}; previously caused a crash in named. This is now rejected earlier by the configuration check. [GL #6330] [GL !12552] Named could crash with a single-element geoip sortlist. 0e996a4d3b If named was configured with a single-element sortlist containing a geoip ACL element, any matching query triggered an assertion failure. This has been fixed. [GL #6342] [GL !12583] Prevent out-of-bailiwick CNAMEs from evicting cached records. cdedd4acd5 A recursive resolver could remove valid cached records when a DNS response contained an out-of-bailiwick CNAME with the same owner name. Out-of-bailiwick data is now discarded before it can modify the cache. [GL #6345] [GL !12651] Restore periodic cleanup of stale resolver address data. 356f4013f8 Stale resolver address data could remain cached until memory pressure or an explicit flush. Correct the cleanup interval so it is removed periodically. [GL #6346] [GL !12589] Fix named-checkconf/named crash with malformed key name. 9f218f6aaf When a primary/remote-server key name was malformed, named-checkconf and named were both crashing (after warning about the invalid key name). This is now fixed. [GL #6362] [GL !12639] Fix -Wformat-truncation warning in totext_in_wks() f97c2bea40 BIND 9 failed to build with GCC 16 at -O3: rendering a WKS record as text triggered a -Wformat-truncation error, which is fatal in developer builds. The port number is now printed with a 16-bit format specifier, so the compiler can see it always fits the output buffer. [GL !12542] Fix off-by-one errors caused by magic hardcoded values. 726c6cb795 Fix off-by-one comparinson errors: "named -p http=" dropped the first digit of the given port (for example, "http=8080" selected port 80) and now uses the port as given, and "named-rrchecker -C" compared only part of the "CLASS" prefix when filtering generic class names, which was harmless in practice but is now corrected. [GL !12616] Hmac_verify() now accepts truncated HMACs only when requested. c81b111496 The hmac_verify() function incorrectly compares only up to 'sig->length' bytes, but the signature and its length should not be trusted, e.g. in case if it comes from a user query. Don't accept signatures which length isn't equal to the expected calculated HMAC length unless it is explicitly requested by the caller, e.g. for truncated TSIG [1] support. [1] https://datatracker.ietf.org/doc/html/rfc8945#name-tsig-truncation -policy [GL !12629] Prevent resolver crashes while processing DNS over TCP. 81b3b6d89f Recursive resolvers could terminate with an assertion failure while processing DNS responses over TCP under sustained traffic. The failure was observed on resolvers configured globally with forward only; the same transport path is also used by iterative resolution. This has been fixed. [GL !12537] @ text @a34 6 # The attacker-controlled sibling zone: a genuine, correctly delegated and # signed zone under the same parent as CHILD. Its crafted NSEC3 is made # to sort first in the ncache via the salt choice below (see _ordering_salts). SIBLING = f"attacker.{PARENT}" # #5967 (grandparent-zone NSEC/NSEC3): a grandchild whose forged NSEC/NSEC3 # insecure-delegation proof is owned by its grandparent zone. a36 14 # #6234 (sibling-zone NSEC3): a grandchild whose forged NSEC3 # insecure-delegation proof is owned by an unrelated but correctly delegated # and signed sibling zone. GRANDCHILD3_SIBLING = f"grandsib.{CHILD}" # #6321 (mixed-signer RRSIG): grandchildren whose grandparent-signed NSEC # forgery also carries a dummy RRSIG naming the NSEC owner itself as signer, # in either order relative to the genuine one. GRANDCHILD_DUMMY_FIRST = f"grand-dummy-first.{CHILD}" GRANDCHILD_DUMMY_LAST = f"grand-dummy-last.{CHILD}" # RRSIG count cap: grandchildren whose grandparent-signed NSEC forgery carries # as many same-signer RRSIGs as ns2 allows validations per fetch, or one fewer. GRANDCHILD_TOO_MANY = f"grand-too-many.{CHILD}" GRANDCHILD_ALMOST_TOO_MANY = f"grand-almost-too-many.{CHILD}" # The names under attack. d40 1 a40 5 # Not a DNSSEC algorithm; the validator skips RRSIGs using it as unsupported # rather than rejecting them, which is what the mixed-signer forgery needs. DUMMY_ALGORITHM = 0 # ns2's max-validations-per-fetch; keep in sync with the test module. MAX_VALIDATIONS_PER_FETCH = 16 d54 1 a54 20 def _ordering_salts(qname: str) -> tuple[str, str]: # Pick NSEC3 salts (as hex) so the sibling's owner hash sorts strictly # before the child's. The ncache slab is ordered by wire-format owner # name -- i.e. by the leftmost hash label -- and is_insecure_referral()'s # trynsec3 arm returns on the *first* exact hash match, so the sibling's # crafted NS-set NSEC3 must precede the child's genuine NS-clear NODATA # proof. Deterministic search over one-octet salts keeps this true no # matter what GRANDCHILD3_SIBLING is named. hashes = sorted( (dns.dnssec.nsec3_hash(name(qname), f"{i:02X}", 0, 1).lower(), f"{i:02X}") for i in range(256) ) return hashes[0][1], hashes[-1][1] # Sibling salt yields the smallest hash, child salt the largest. SIBLING_SALT, CHILD_SALT = _ordering_salts(GRANDCHILD3_SIBLING) def load_keys() -> dict[str, Key]: d57 1 a57 1 raw = json.load(keys_file) d59 8 a66 11 keys: dict[str, Key] = {} for zone, raw_key in raw.items(): private_key = serialization.load_pem_private_key( raw_key["private_pem"].encode("ascii"), password=None, ) dnskey = dns.rdata.from_text( dns.rdataclass.IN, dns.rdatatype.DNSKEY, raw_key["dnskey"] ) keys[zone] = Key(name(zone), private_key, dnskey) return keys d77 4 a80 2 def sign(covered: dns.rrset.RRset, signer: Key) -> dns.rdata.Rdata: return dns.dnssec.sign( a87 6 def add_signed( section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key ) -> None: rrsig = sign(covered, signer) d104 2 a105 13 def child_soa_rrset() -> dns.rrset.RRset: return rrset( CHILD, dns.rdatatype.SOA, f"ns.{CHILD} hostmaster.{CHILD} 1 3600 600 86400 300", ) def nsec_lie(owner: str) -> dns.rrset.RRset: # An NSEC owned by the grandparent zone P at a name that really belongs # to the secure child C, showing an (insecure) delegation: NS bit set, # DS bit clear. return nsec_rrset(owner, f"grandz.{CHILD}", "NS", "RRSIG", "NSEC") d109 1 a109 25 return nsec_lie(GRANDCHILD) def nsec3_ns_lie( qname: str, zone: str, salt: str | None, salt_text: str ) -> dns.rrset.RRset: # An NSEC3 owned by 'zone' whose owner hash matches 'qname' under this # record's own parameters, showing an (insecure) delegation: NS bit set, DS # bit clear. is_insecure_referral()'s trynsec3 arm takes the exact-match # branch (order == 0) before it ever consults the "next" field, so reusing # the owner digest as the next hash is sufficient for it to parse. digest = dns.dnssec.nsec3_hash(name(qname), salt, 0, 1).lower() owner = f"{digest}.{zone}" return rrset(owner, dns.rdatatype.NSEC3, f"1 0 0 {salt_text} {digest} NS") def nsec3_nodata( qname: str, zone: str, salt: str | None, salt_text: str ) -> dns.rrset.RRset: # A genuine matching NSEC3 for 'qname' in 'zone' with the DS bit clear: a # legitimate NODATA-DS proof (the name exists as an ordinary, non-delegation # node). The NS bit is clear, so it does not itself assert a delegation. digest = dns.dnssec.nsec3_hash(name(qname), salt, 0, 1).lower() owner = f"{digest}.{zone}" return rrset(owner, dns.rdatatype.NSEC3, f"1 0 0 {salt_text} {digest} TXT RRSIG") d113 7 a119 4 # Same forgery as grandchild_nsec_lie(), but expressed as an NSEC3 signed by # the grandparent so that the resolver reaches is_insecure_referral()'s # trynsec3 arm. return nsec3_ns_lie(GRANDCHILD3, PARENT, None, "-") a128 83 def add_nsec3_nodata_from_sibling( response: dns.message.Message, child_key: Key, sibling_key: Key ) -> None: # #6234: a genuinely signed NSEC3 owned by an unrelated sibling zone whose # owner hash matches the grandchild under the sibling's own parameters and # whose NS bit is set. Its owner hash sorts before the child proof's (the # salts are chosen for exactly that, see _ordering_salts), so the ncache # iterates it first; trynsec3 matches it and derives the signer as # owner-minus-hash-label -> SIBLING (4 labels), which empties # closer_secure_ds_exists(). No owner-zone relevance check rejects it. The # child-signed NSEC3 that follows is the real NODATA-DS proof: # dns_nsec3_noexistnodata() ignores the sibling record as out-of-zone, so # the negative answer still validates normally. add_signed( response.authority, nsec3_ns_lie(GRANDCHILD3_SIBLING, SIBLING, SIBLING_SALT, SIBLING_SALT), sibling_key, ) add_signed( response.authority, nsec3_nodata(GRANDCHILD3_SIBLING, CHILD, CHILD_SALT, CHILD_SALT), child_key, ) add_signed(response.authority, child_soa_rrset(), child_key) def add_mixed_signer_nodata( response: dns.message.Message, parent_key: Key, nsec: dns.rrset.RRset, dummy_first: bool, ) -> None: """ The same NODATA lie as add_parent_nodata(), but the NSEC carries two RRSIGs: the genuine one from the grandparent P and a dummy one naming the NSEC owner itself as signer. The dummy uses an unsupported algorithm, so the validator skips it and the NSEC still authenticates through the genuine RRSIG. All the dummy changes is which signer name comes first in the RRSIG rdataset (#6321). """ add_signed(response.authority, soa_rrset(), parent_key) genuine = sign(nsec, parent_key) dummy = genuine.replace(algorithm=DUMMY_ALGORITHM, signer=nsec.name) rrsigs = [dummy, genuine] if dummy_first else [genuine, dummy] response.authority.append(nsec) # One single-rdata RRset per RRSIG: dnspython shuffles the rdatas of an # rdataset when rendering it, and this forgery is all about the order # in which the two signatures arrive. Separate RRsets keep their list # order on the wire, and the resolver merges them back into one RRSIG # rdataset in that order. for rrsig in rrsigs: response.authority.append(dns.rrset.from_rdata(nsec.name, nsec.ttl, rrsig)) def add_many_rrsig_nodata( response: dns.message.Message, parent_key: Key, nsec: dns.rrset.RRset, count: int, ) -> None: """ The NODATA lie with 'count' RRSIGs over the NSEC, all naming the grandparent P as signer: count - 1 unsupported-algorithm dummies with distinct key tags and a one-byte signature, then the genuine signature last, so the validator has to skip every dummy before the NSEC authenticates. With a uniform signer this exercises only the RRSIG count cap in is_insecure_referral(), not the mixed-signer rule. """ add_signed(response.authority, soa_rrset(), parent_key) genuine = sign(nsec, parent_key) dummies = [ genuine.replace(algorithm=DUMMY_ALGORITHM, key_tag=tag, signature=b"\0") for tag in range(count - 1) ] response.authority.append(nsec) # Separate single-rdata RRsets, for the same wire-order reason as in # add_mixed_signer_nodata(). for rrsig in [*dummies, genuine]: response.authority.append(dns.rrset.from_rdata(nsec.name, nsec.ttl, rrsig)) d137 2 a138 4 def __init__(self, keys: dict[str, Key]) -> None: self.parent_key = keys[PARENT] self.child_key = keys[CHILD] self.sibling_key = keys[SIBLING] a140 1 self.sibling = name(SIBLING) a142 14 self.grandchild3_sibling = name(GRANDCHILD3_SIBLING) self.grandchild_dummy_first = name(GRANDCHILD_DUMMY_FIRST) self.grandchild_dummy_last = name(GRANDCHILD_DUMMY_LAST) self.grandchild_too_many = name(GRANDCHILD_TOO_MANY) self.grandchild_almost_too_many = name(GRANDCHILD_ALMOST_TOO_MANY) self.forged_grandchildren = ( self.grandchild, self.grandchild3, self.grandchild3_sibling, self.grandchild_dummy_first, self.grandchild_dummy_last, self.grandchild_too_many, self.grandchild_almost_too_many, ) d163 2 a164 40 # Priming, child DS. # # A real DS matching the child key, signed by the parent. It must # be real rather than a placeholder because the sibling-zone # variant includes an NSEC3 signed by the child, so the child's # DNSKEY has to chain to the parent. It is also the secure DS at # CHILD that closer_secure_ds_exists() finds when it refuses the # grandparent-signed proofs of the #5967 variants. ds = dns.dnssec.make_ds(self.child, self.child_key.dnskey, "SHA256") add_signed( response.answer, dns.rrset.from_rdata(self.child, TTL, ds), self.parent_key, ) elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.DNSKEY: # Priming, child DNSKEY. add_signed( response.answer, rrset_from_rdata(CHILD, self.child_key.dnskey), self.child_key, ) elif qctx.qname == self.sibling and qctx.qtype == dns.rdatatype.DS: # Priming, sibling DS. # # The sibling zone is a genuine secure delegation: real DS signed by # the parent, so its own NSEC3 (used in the sibling-zone attack # variant) really validates. ds = dns.dnssec.make_ds(self.sibling, self.sibling_key.dnskey, "SHA256") add_signed( response.answer, dns.rrset.from_rdata(self.sibling, TTL, ds), self.parent_key, ) elif qctx.qname == self.sibling and qctx.qtype == dns.rdatatype.DNSKEY: # Priming, sibling DNSKEY. add_signed( response.answer, rrset_from_rdata(SIBLING, self.sibling_key.dnskey), self.sibling_key, ) d166 1 a166 1 # #5967: Forge no data for grand child DS (NSEC variant). d169 1 a169 1 # #5967: Forge no data for grand child DS (NSEC3 variant). a170 44 elif qctx.qname == self.grandchild3_sibling and qctx.qtype == dns.rdatatype.DS: # #6234: Sibling-zone-signed NSEC3 ahead of the real child proof. add_nsec3_nodata_from_sibling(response, self.child_key, self.sibling_key) elif ( qctx.qname == self.grandchild_dummy_first and qctx.qtype == dns.rdatatype.DS ): # Forge no data for grand child DS, dummy RRSIG before the # genuine one (mixed-signer variant, #6321) add_mixed_signer_nodata( response, self.parent_key, nsec_lie(GRANDCHILD_DUMMY_FIRST), dummy_first=True, ) elif ( qctx.qname == self.grandchild_dummy_last and qctx.qtype == dns.rdatatype.DS ): # Same forgery, genuine RRSIG before the dummy one add_mixed_signer_nodata( response, self.parent_key, nsec_lie(GRANDCHILD_DUMMY_LAST), dummy_first=False, ) elif qctx.qname == self.grandchild_too_many and qctx.qtype == dns.rdatatype.DS: # Forge no data for grand child DS with as many RRSIGs as ns2 # allows validations per fetch (RRSIG count cap variant) add_many_rrsig_nodata( response, self.parent_key, nsec_lie(GRANDCHILD_TOO_MANY), count=MAX_VALIDATIONS_PER_FETCH, ) elif ( qctx.qname == self.grandchild_almost_too_many and qctx.qtype == dns.rdatatype.DS ): # Same forgery with one RRSIG fewer, so it stays under the cap add_many_rrsig_nodata( response, self.parent_key, nsec_lie(GRANDCHILD_ALMOST_TOO_MANY), count=MAX_VALIDATIONS_PER_FETCH - 1, ) d172 3 a174 3 any(qctx.qname.is_subdomain(g) for g in self.forged_grandchildren) and qctx.qtype == dns.rdatatype.A ): d187 1 a187 1 server.install_response_handlers(GrandparentNsecHandler(load_keys())) @