fix: bound NSEC bitmap length against record end#1731
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1731 +/- ##
=======================================
Coverage 99.76% 99.76%
=======================================
Files 33 33
Lines 3440 3444 +4
Branches 473 475 +2
=======================================
+ Hits 3432 3436 +4
Misses 5 5
Partials 3 3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Fixes a packet-parsing integrity issue in DNSIncoming._read_bitmap for NSEC records by validating bitmap window boundaries against the record’s declared RDATA end, preventing offset corruption that could break parsing of subsequent records and pollute cached NSEC “absent type” bits.
Changes:
- Add strict bounds checks in
_read_bitmap(window header fits, bitmap length is 1..32, bitmap does not overrun the record end) and raiseIncomingDecodeErroron violations so_read_othersskips the malformed record safely. - Update the Cython
.pxdlocals to keepbitmap_endunsigned for correct comparisons in compiled builds. - Add regression tests ensuring malformed NSEC bitmaps are rejected while subsequent records in the same packet still parse.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/zeroconf/_protocol/incoming.py |
Adds record-end bounds validation for NSEC bitmap parsing and raises IncomingDecodeError to prevent offset corruption. |
src/zeroconf/_protocol/incoming.pxd |
Declares bitmap_end as unsigned int for Cython-compiled parity and correct unsigned comparisons. |
tests/test_protocol.py |
Adds regression tests for NSEC bitmap overrun and zero-length window handling, ensuring later records still parse. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
coverage is incomplete |
PR Review — fix: bound NSEC bitmap length against record endThe fix itself is correct and well-targeted: validating the window header fits inside the record, rejecting bitmap_length outside RFC 4034 §4.1.2 bounds, and rejecting bitmap_end > end is the right shape, and raising IncomingDecodeError relies on _read_others to reset self.offset to end — exactly the recovery path the issue describes. The .pxd update keeps bitmap_end unsigned end-to-end, matching the existing convention. Two things must be addressed before merge: (1) the unrelated .github/workflows/ci.yml cache-key bump that @bdraco has now asked twice to remove, and (2) the still-incomplete branch coverage on the third disjunct of the guard (bitmap_end > end with a valid 1..32 bitmap_length is never exercised because Python short-circuits at the > 32 check in the 255-byte test). 🔴 Blocking1. Remove unrelated CI cache-key bump (`.github/workflows/ci.yml`, L102-141)@bdraco has explicitly asked twice for the 🟡 Important1. Missing branch coverage for `bitmap_end > end` with a valid bitmap_length (`tests/test_protocol.py`, L808-886)@bdraco flagged coverage as incomplete twice (comments 4475236178 and 4490613964). Looking at the guard: if bitmap_length == 0 or bitmap_length > 32 or bitmap_end > end:The three existing tests cover:
What's not directly covered is the 🟢 Suggestions1. Comment is a little long for the local style (`src/zeroconf/_protocol/incoming.py`, L391-407)CLAUDE.md says comments default to one line and exist to surface the non-obvious why. The 4-line block here mixes RFC citation (worth keeping) with a narrative restatement of the bug ("would otherwise leave self.offset pointing inside (or past) the next record header, corrupting every subsequent record"). The narrative belongs in the commit message / PR description (and is already there). A tighter version — keeping the RFC citation and dropping the post-mortem — would read better and age more gracefully: # RFC 4034 §4.1.2: each window block is window-number + bitmap-length
# (1..32) + bitmap. Reject windows that walk past the record end.Non-blocking, but it's the kind of trim the project's CLAUDE.md asks for. 2. Test docstrings retell the production-side story (`tests/test_protocol.py`, L810)CLAUDE.md: "A test docstring should name what the test pins, in one sentence — not re-explain the bug, the fix, or the surrounding flow." The current docstrings ( Checklist
SummaryThe fix itself is correct and well-targeted: validating the window header fits inside the record, rejecting bitmap_length outside RFC 4034 §4.1.2 bounds, and rejecting bitmap_end > end is the right shape, and raising IncomingDecodeError relies on _read_others to reset self.offset to end — exactly the recovery path the issue describes. The .pxd update keeps bitmap_end unsigned end-to-end, matching the existing convention. Two things must be addressed before merge: (1) the unrelated .github/workflows/ci.yml cache-key bump that @bdraco has now asked twice to remove, and (2) the still-incomplete branch coverage on the third disjunct of the guard (bitmap_end > end with a valid 1..32 bitmap_length is never exercised because Python short-circuits at the > 32 check in the 255-byte test). To rebase specific severity levels, mention me: |
Confirmed — Codecov's 50% /
Adding those two cases gets the patch back to 100% and also gives each failure mode a distinct error message in the logs. |
Rebase with requested adjustmentsBranch StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
505b2c2 to
495acf5
Compare
|
Coverage is still incomplete |
Confirmed — the gap is in two specific branches of the new guards:
Adding those two NSEC packets as a third and fourth test should close out the codecov delta and lock the guard branches in place against future refactors. |
Rebase with requested adjustmentsBranch StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
495acf5 to
9d58982
Compare
|
@bluetoothbot rebase |
A malicious NSEC bitmap_length that overruns the declared record end left self.offset pointing past the next record's header, corrupting every subsequent record in the same packet. The forgiving Python slice also let the parser synthesize attacker-controlled rdtypes from adjacent bytes and cache them on the resulting DNSNsec. Validate the window header is in-record, reject bitmap_length 0 or > 32 (RFC 4034 §4.1.2), and raise IncomingDecodeError so _read_others resets the offset to the NSEC's declared end.
Rebase with requested adjustmentsBranch StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
8bb1c38 to
98615d9
Compare
|
remove the unrelated changes in .github/workflows/ci.yml from the pr. that problem was already fixed |
The codecov report shows 100% line coverage for the modified lines, so this is most likely branch coverage on the combined |
Agreed — the |
Rebase with requested adjustmentsBranch StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Summary
_read_bitmapread an attacker-controlledbitmap_lengthbyte and unconditionally advancedself.offsetby2 + bitmap_length, with no check against the NSEC record's declared end. Because Python slicing tolerates overshoot, the bitmap silently consumed adjacent bytes (the next record's header) as bitmap data and leftself.offsetpastend._read_othersonly resets the offset on exception, so a malformed-but-non-throwing NSEC corrupted the parse for every subsequent record in the same packet and let a LAN peer pollute the NSEC cache with attacker-chosen "absent" type bits — enough to talk anAddressResolverout of issuing follow-up queries.Fixes #1725
Changes
bitmap_lengthof 0 or > 32 (RFC 4034 §4.1.2 bounds the per-window bitmap to 1..32 bytes).bitmap_end > endso a window cannot reach past the NSEC record's declared length.IncomingDecodeErrorso_read_othersresetsself.offsettoendand the next record parses normally.bitmap_endasunsigned intin the.pxdso the Cython compare againstendstays unsigned end-to-end.Test plan
test_nsec_bitmap_length_overruns_record_end— crafts an NSEC withbitmap_length=255and a trailing PTR; the PTR must still parse and the NSEC must be skipped. Fails onmaster, passes with the fix.test_nsec_bitmap_zero_length_window_rejected— zero-length window block rejected per RFC 4034 §4.1.2.test_parse_packet_with_nsec_record,test_dns_compression_invalid_skips_record,test_dns_compression_points_forward, andtest_parse_own_packet_nsecstill pass — valid bitmaps unaffected.SKIP_CYTHON=1 poetry run pytest tests/test_protocol.py tests/test_handlers.py tests/test_services.py→ 84 passed.Quality Report
Changes: 3 files changed, 69 insertions(+)
Code scan: clean
Tests: passed (4 PASSED)
Branch hygiene: clean
Generated by Kōan post-mission quality pipeline