From 497263831a678aa573c34554bb843df1943f37b5 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Sat, 15 Nov 2025 07:53:25 -0800 Subject: [PATCH] Zone transfers should ignore glue that is not a subdomain of the origin [#1236]. Ideally, we'd actually store this somewhere as in theory it is needed for pathological cross zone dependencies. We are not doing this as in practice such dependencies break a lot of DNS software, and are usually not viable "in the wild". --- dns/xfr.py | 11 ++++++++++- dns/zone.py | 2 -- tests/test_xfr.py | 26 ++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/dns/xfr.py b/dns/xfr.py index 219fdc8c..d0f44da8 100644 --- a/dns/xfr.py +++ b/dns/xfr.py @@ -92,7 +92,10 @@ class Inbound: raise ValueError("rdtype is not IXFR or AXFR") self.serial = serial self.is_udp = is_udp - (_, _, self.origin) = txn_manager.origin_information() + (_, _, origin) = txn_manager.origin_information() + if origin is None: + raise ValueError("transaction manager must supply an origin for XFRs") + self.origin = origin self.soa_rdataset: dns.rdataset.Rdataset | None = None self.done = False self.expecting_SOA = False @@ -232,6 +235,12 @@ class Inbound: # Note we are falling through into the code below # so whatever rdataset this was gets written. # + # Ignore glue that is not a subdomain of the origin. For pathological + # cases it would be good if we could keep it in some side location, but + # dnspython zones don't have a place or an API for that, so we just ignore + # it at this time. + if not name.is_subdomain(self.origin): + continue # Add or remove the data if self.delete_mode: self.txn.delete_exact(name, rdataset) diff --git a/dns/zone.py b/dns/zone.py index f916ffee..0dfecc92 100644 --- a/dns/zone.py +++ b/dns/zone.py @@ -34,7 +34,6 @@ from typing import ( ) import dns.exception -import dns.grange import dns.immutable import dns.name import dns.node @@ -47,7 +46,6 @@ import dns.rdtypes.ANY.ZONEMD import dns.rrset import dns.tokenizer import dns.transaction -import dns.ttl import dns.zonefile from dns.zonetypes import DigestHashAlgorithm, DigestScheme, _digest_hashers diff --git a/tests/test_xfr.py b/tests/test_xfr.py index 257397c2..10b33a5b 100644 --- a/tests/test_xfr.py +++ b/tests/test_xfr.py @@ -84,6 +84,22 @@ example. IN AXFR @ 3600 IN SOA foo bar 1 2 3 4 7 """ +axfr_with_nonsubdomain_glue = """id 1 +opcode QUERY +rcode NOERROR +flags AA +;QUESTION +example. IN AXFR +;ANSWER +@ 3600 IN SOA foo bar 1 2 3 4 5 +bar.foo 300 IN MX 0 blaz.foo +ns1 3600 IN A 10.0.0.1 +ns2 3600 IN A 10.0.0.2 +sub 300 IN NS ns1.other. +ns1.other. 300 IN A 1.2.3.4 +@ 3600 IN SOA foo bar 1 2 3 4 5 +""" + ixfr = """id 1 opcode QUERY rcode NOERROR @@ -274,6 +290,7 @@ example. IN IXFR @ 3600 IN NS ns1 @ 3600 IN NS ns2 """ + ixfr_axfr2 = """id 1 opcode QUERY rcode NOERROR @@ -331,6 +348,15 @@ def test_axfr_unexpected_origin(): xfr.process_message(m) +def test_axfr_with_non_subdomain_glue(): + z = dns.versioned.Zone("example.") + m = dns.message.from_text( + axfr_with_nonsubdomain_glue, origin=z.origin, one_rr_per_rrset=True + ) + with dns.xfr.Inbound(z, dns.rdatatype.AXFR) as xfr: + xfr.process_message(m) + + def test_basic_ixfr(): z = dns.zone.from_text(base, "example.", zone_factory=dns.versioned.Zone) m = dns.message.from_text(ixfr, origin=z.origin, one_rr_per_rrset=True) -- 2.47.3