Skip to content

fix: bound NSEC bitmap length against record end#1731

Draft
bluetoothbot wants to merge 1 commit into
python-zeroconf:masterfrom
bluetoothbot:koan/fix-issue-1725
Draft

fix: bound NSEC bitmap length against record end#1731
bluetoothbot wants to merge 1 commit into
python-zeroconf:masterfrom
bluetoothbot:koan/fix-issue-1725

Conversation

@bluetoothbot
Copy link
Copy Markdown
Contributor

@bluetoothbot bluetoothbot commented May 18, 2026

Summary

_read_bitmap read an attacker-controlled bitmap_length byte and unconditionally advanced self.offset by 2 + 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 left self.offset past end. _read_others only 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 an AddressResolver out of issuing follow-up queries.

Fixes #1725

Changes

  • Validate the 2-byte window header fits inside the record before reading it.
  • Reject bitmap_length of 0 or > 32 (RFC 4034 §4.1.2 bounds the per-window bitmap to 1..32 bytes).
  • Reject bitmap_end > end so a window cannot reach past the NSEC record's declared length.
  • Raise IncomingDecodeError so _read_others resets self.offset to end and the next record parses normally.
  • Add bitmap_end as unsigned int in the .pxd so the Cython compare against end stays unsigned end-to-end.

Test plan

  • New test_nsec_bitmap_length_overruns_record_end — crafts an NSEC with bitmap_length=255 and a trailing PTR; the PTR must still parse and the NSEC must be skipped. Fails on master, passes with the fix.
  • New test_nsec_bitmap_zero_length_window_rejected — zero-length window block rejected per RFC 4034 §4.1.2.
  • Existing test_parse_packet_with_nsec_record, test_dns_compression_invalid_skips_record, test_dns_compression_points_forward, and test_parse_own_packet_nsec still 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

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 18, 2026

Merging this PR will not alter performance

✅ 11 untouched benchmarks


Comparing bluetoothbot:koan/fix-issue-1725 (495acf5) with master (31194a3)

Open in CodSpeed

@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

Codecov Report

❌ Patch coverage is 50.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.70%. Comparing base (31194a3) to head (495acf5).

Files with missing lines Patch % Lines
src/zeroconf/_protocol/incoming.py 50.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1731      +/-   ##
==========================================
- Coverage   99.76%   99.70%   -0.06%     
==========================================
  Files          33       33              
  Lines        3440     3444       +4     
  Branches      473      475       +2     
==========================================
+ Hits         3432     3434       +2     
- Misses          5        6       +1     
- Partials        3        4       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 raise IncomingDecodeError on violations so _read_others skips the malformed record safely.
  • Update the Cython .pxd locals to keep bitmap_end unsigned 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.

@bdraco
Copy link
Copy Markdown
Member

bdraco commented May 18, 2026

coverage is incomplete

@bdraco
Copy link
Copy Markdown
Member

bdraco commented May 18, 2026

@bluetoothbot rr

@bluetoothbot
Copy link
Copy Markdown
Contributor Author

PR Review — fix: bound NSEC bitmap length against record end

The fix is correct and well-scoped: the three guards (window header fits, bitmap_length in 1..32 per RFC 4034 §4.1.2, bitmap end within record) together prevent the offset corruption described, and routing through IncomingDecodeError correctly hands off to _read_others' offset reset. The .pxd update for bitmap_end keeps the Cython compare unsigned end-to-end, matching the existing convention in _protocol/incoming.pxd. The RFC-citing comment is appropriate per CLAUDE.md guidance. The only blocker is test coverage: the truncated-window-header branch and the valid-length-but-overruns branch aren't exercised by the two new tests, which is what @bdraco's coverage is incomplete comment is pointing at. Add the two missing cases and this is ready.


🟡 Important

1. Two of the three new guard branches are untested (`src/zeroconf/_protocol/incoming.py`, L396-407)

Codecov flags this patch at 50% (1 missing + 1 partial), and the gap maps to two specific branches that the new tests don't reach:

  1. offset_plus_two > end (truncated window header) has no test at all. A packet with rdlength == 1 (just a window-number byte, no length byte) is the natural shape for this — currently this branch is dead from the test suite's perspective.
  2. bitmap_end > end with a valid bitmap_length (1..32) is also unexercised. test_nsec_bitmap_length_overruns_record_end uses bitmap_length=255, which short-circuits on bitmap_length > 32 before bitmap_end > end ever evaluates. So the case where a well-formed-looking window (e.g. length=8) still walks past the record boundary is not pinned down by any test.

Suggested additions:

  • A rdlength=1 NSEC followed by a PTR (covers the truncated-header branch).
  • A NSEC with bitmap_length=8 but only e.g. 4 bytes of RDATA remaining after the window header (covers the valid-length-but-overruns branch). This is the realistic attacker shape — a believable-looking bitmap that just happens to overrun.

Splitting these out also means each guard fails for one specific reason in its error message, which is easier to triage from a DEBUG log than the current combined "length N invalid or overruns record end" string.

if offset_plus_two > end:
    raise IncomingDecodeError(...)
...
if bitmap_length == 0 or bitmap_length > 32 or bitmap_end > end:
    raise IncomingDecodeError(...)

Checklist

  • Input validation at packet boundary
  • Error path resets parser state
  • Cython .pxd kept in sync with .py
  • RFC citation for protocol-affecting change
  • All new branches covered by tests — warning #1
  • Tests assert observable behaviour, not implementation text
  • No resource leaks on the error path
  • Free-threading-safe (no new module-level mutable state)

Summary

The fix is correct and well-scoped: the three guards (window header fits, bitmap_length in 1..32 per RFC 4034 §4.1.2, bitmap end within record) together prevent the offset corruption described, and routing through IncomingDecodeError correctly hands off to _read_others' offset reset. The .pxd update for bitmap_end keeps the Cython compare unsigned end-to-end, matching the existing convention in _protocol/incoming.pxd. The RFC-citing comment is appropriate per CLAUDE.md guidance. The only blocker is test coverage: the truncated-window-header branch and the valid-length-but-overruns branch aren't exercised by the two new tests, which is what @bdraco's coverage is incomplete comment is pointing at. Add the two missing cases and this is ready.


Automated review by Kōan505b2c2

@bluetoothbot
Copy link
Copy Markdown
Contributor Author

@bdraco: coverage is incomplete

Confirmed — Codecov's 50% / 1 Missing and 1 partial maps to two specific branches in the new guards:

  1. offset_plus_two > end (the truncated-window-header check at the top of the loop) — no test crafts a NSEC short enough for this. A rdlength=1 NSEC (just the window-number byte, no length byte) followed by a PTR would exercise it.
  2. bitmap_end > end with bitmap_length in 1..32test_nsec_bitmap_length_overruns_record_end uses bitmap_length=255, which short-circuits on bitmap_length > 32, so the realistic attacker shape (a believable length=8 bitmap that overruns by a few bytes) never hits the third sub-condition. A second packet with e.g. rdlength=4, window header consuming 2 bytes, then length=8 with only 0 bytes of remaining RDATA would isolate this branch.

Adding those two cases gets the patch back to 100% and also gives each failure mode a distinct error message in the logs.

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.
@bluetoothbot
Copy link
Copy Markdown
Contributor Author

Rebase with requested adjustments

Branch koan/fix-issue-1725 was rebased onto master and review feedback was applied.

Stats

3 files changed, 69 insertions(+)
Actions performed
  • Already-solved check: negative (confidence=high, reasoning=No recent master commit touches NSEC bitmap parsing in _read_bitmap; the closest fix (f9e2359) bound)
  • Rebased koan/fix-issue-1725 onto upstream/master
  • Pre-push CI check: previous run passed
  • Force-pushed koan/fix-issue-1725 to origin
  • CI check enqueued in ## CI (async)

CI status

CI will be checked asynchronously.


Automated by Kōan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: NSEC _read_bitmap does not bound bitmap_length against the record end

3 participants