@@ -807,6 +807,85 @@ def test_parse_packet_with_nsec_record():
807807 assert nsec_record .next_name == "MyHome54 (2)._meshcop._udp.local."
808808
809809
810+ def test_nsec_bitmap_length_overruns_record_end ():
811+ """Reject NSEC bitmap whose declared length runs past the record boundary."""
812+ # 0 questions, 2 answers. Answer 1 is a malformed NSEC: rdlength=9, but the
813+ # bitmap window claims length=255 — overrunning the record. Answer 2 is a
814+ # PTR that must still parse because the offset for the next record stays
815+ # pinned to the NSEC's declared end.
816+ packet = (
817+ b"\x00 \x00 \x84 \x00 \x00 \x00 \x00 \x02 \x00 \x00 \x00 \x00 "
818+ b"\x04 test\x05 local\x00 "
819+ b"\x00 \x2f \x80 \x01 "
820+ b"\x00 \x00 \x11 \x94 "
821+ b"\x00 \x09 "
822+ b"\xc0 \x0c "
823+ b"\x00 \xff "
824+ b"\x80 \x00 \x00 \x00 \x00 "
825+ b"\xc0 \x0c "
826+ b"\x00 \x0c \x00 \x01 "
827+ b"\x00 \x00 \x11 \x94 "
828+ b"\x00 \x02 "
829+ b"\xc0 \x0c "
830+ )
831+ parsed = r .DNSIncoming (packet )
832+ answers = parsed .answers ()
833+ ptrs = [a for a in answers if isinstance (a , r .DNSPointer )]
834+ assert len (ptrs ) == 1
835+ assert ptrs [0 ].alias == "test.local."
836+ # The malformed NSEC must not surface — if it did, it would carry rdtypes
837+ # synthesized from bytes past the record boundary.
838+ assert not any (isinstance (a , r .DNSNsec ) for a in answers )
839+
840+
841+ def test_nsec_bitmap_zero_length_window_rejected ():
842+ """A bitmap window with length=0 violates RFC 4034 §4.1.2 and must be rejected."""
843+ packet = (
844+ b"\x00 \x00 \x84 \x00 \x00 \x00 \x00 \x02 \x00 \x00 \x00 \x00 "
845+ b"\x04 test\x05 local\x00 "
846+ b"\x00 \x2f \x80 \x01 "
847+ b"\x00 \x00 \x11 \x94 "
848+ b"\x00 \x04 "
849+ b"\xc0 \x0c "
850+ b"\x00 \x00 "
851+ b"\xc0 \x0c "
852+ b"\x00 \x0c \x00 \x01 "
853+ b"\x00 \x00 \x11 \x94 "
854+ b"\x00 \x02 "
855+ b"\xc0 \x0c "
856+ )
857+ parsed = r .DNSIncoming (packet )
858+ answers = parsed .answers ()
859+ ptrs = [a for a in answers if isinstance (a , r .DNSPointer )]
860+ assert len (ptrs ) == 1
861+ assert not any (isinstance (a , r .DNSNsec ) for a in answers )
862+
863+
864+ def test_nsec_bitmap_truncated_window_header_rejected ():
865+ """Reject NSEC bitmap with a trailing byte too short to hold a window header."""
866+ packet = (
867+ b"\x00 \x00 \x84 \x00 \x00 \x00 \x00 \x02 \x00 \x00 \x00 \x00 "
868+ b"\x04 test\x05 local\x00 "
869+ b"\x00 \x2f \x80 \x01 "
870+ b"\x00 \x00 \x11 \x94 "
871+ b"\x00 \x06 "
872+ b"\xc0 \x0c "
873+ b"\x00 \x01 \x80 "
874+ b"\xff "
875+ b"\xc0 \x0c "
876+ b"\x00 \x0c \x00 \x01 "
877+ b"\x00 \x00 \x11 \x94 "
878+ b"\x00 \x02 "
879+ b"\xc0 \x0c "
880+ )
881+ parsed = r .DNSIncoming (packet )
882+ answers = parsed .answers ()
883+ ptrs = [a for a in answers if isinstance (a , r .DNSPointer )]
884+ assert len (ptrs ) == 1
885+ assert ptrs [0 ].alias == "test.local."
886+ assert not any (isinstance (a , r .DNSNsec ) for a in answers )
887+
888+
810889def test_records_same_packet_share_fate ():
811890 """Test records in the same packet all have the same created time."""
812891 out = r .DNSOutgoing (const ._FLAGS_QR_QUERY | const ._FLAGS_AA )
0 commit comments