Skip to content

Bound DNS RDATA character-string decoding by the declared RDATA length - #420

Open
mattrm456 wants to merge 1 commit into
bloomberg:mainfrom
mattrm456:ntcdns-resilience-malformed-txt-record
Open

mattrm456 wants to merge 1 commit into
bloomberg:mainfrom
mattrm456:ntcdns-resilience-malformed-txt-record

Conversation

@mattrm456

@mattrm456 mattrm456 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This hardens ntcdns::ResourceRecord::decode against malformed RDATA from a malicious or on-path DNS responder. It addresses a report that crafted TXT RDATA could underflow the TXT sub-buffer length and desynchronize the decoder from the RDATA boundary.

The underflow itself was fixed in 6a080a8 ("Fix inverted condition when handling number of bytes read from and DNS TXT record"). Before that commit, a character-string longer than the remaining RDATA wrapped numBytesRemaining, and the loop kept parsing bytes from the following records as TXT data.

Two gaps remained after that fix, and this PR closes them:

  • Character-strings were bounded only by the end of the message, not the end of the RDATA. An oversized string was fully read out of the following bytes before the TXT loop rejected it. HINFO had no per-string RDATA bound at all.
  • Most record types never checked the declared RDATA length up front. Overruns were caught only afterwards by checkCoherentRdataLength, once the decoder had already read past the RDATA.

Changes

ntcdns_protocol.{h,cpp}

  • RDATA must fit in the message. After decoding rdataLength, ResourceRecord::decode checks it against the bytes remaining in the message before any type-specific parsing, for every record type.
  • New MemoryDecoder::decodeCharacterString(bsl::string* value, bsl::size_t limit). It reads the length byte without consuming it. If 1 + length exceeds limit (capped at the remaining buffer), it fails and leaves the decoder where it was. The existing one-argument overload now calls it with the remaining buffer as the limit, so its behaviour is unchanged.
  • TXT: each character-string is decoded with numBytesRemaining as the limit, so a string can no longer cross the RDATA boundary. The existing numBytesRead <= numBytesRemaining check stays as a second line of defence.
  • HINFO: the CPU string is limited to rdataLength and the OS string to what's left of the RDATA.
  • WKS: records with rdataLength shorter than the address plus protocol (5 bytes) are rejected before anything is decoded.

ntcdns_protocol.t.cpp

This adds test coverage for TXT and HINFO, which had none, plus malformed-input cases:

Test Covers
verifyCharacterStringLimit Rejects without advancing when the string exceeds the limit or the limit is zero; accepts an exact fit; caps a limit larger than the buffer
verifyTxt A valid multi-string TXT record, including an empty string; encode/decode round trip
verifyTxtMalformed A string that crosses the RDATA boundary into a valid second answer (the desync scenario); also checks the decoder stops at the start of the RDATA. rdataLength running past the end of the message. rdataLength ending partway through a string
verifyHinfo A valid HINFO record; encode/decode round trip
verifyHinfoMalformed rdataLength ending inside the CPU string, right after it, and inside the OS string
verifyWksMalformed rdataLength shorter than address plus protocol

Two private helpers support these tests: buildResponse, which builds a response whose declared RDATA length and actual payload can differ, and verifyRoundTrip.

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.

1 participant