Skip to content

Reject a hostile Content-Length instead of panicking - #24

Merged
omarroth merged 1 commit into
omarroth:mainfrom
objevovat:reject-bad-content-length
Aug 3, 2026
Merged

Reject a hostile Content-Length instead of panicking#24
omarroth merged 1 commit into
omarroth:mainfrom
objevovat:reject-bad-content-length

Conversation

@objevovat

@objevovat objevovat commented Jul 31, 2026

Copy link
Copy Markdown

The bug

A receiver answering with Content-Length: -1 crashes the sender.

parseHTTPHeader reads the value with Sscanf into an int and doesn't check it. In readPlaintextHTTPResponse the zero case is handled, but a negative value falls through to:

body := make([]byte, contentLength)   // panics: makeslice: len out of range

Nothing recovers it. I reproduced this end to end over a net.Pipe standing in for the receiver — the panic happens inside readPlaintextHTTPResponse, not in a helper.

Also bounded the positive case

Content-Length is receiver-controlled and was used unbounded, so Content-Length: 2147483647 asks the sender for a 2 GB allocation before a single body byte arrives. Control-channel bodies here are small plists, so the 8 MB limit is far above anything legitimate and far below anything that hurts.

Scope

Only these two sites. I checked the other four make([]byte, n) in the package and left them alone:

  • readEncryptedFrame already bounds its uint16 length and rejects anything over 16384
  • hkdfSHA512 and padTo take caller-supplied sizes, not network values
  • audio.go's n comes from a Read, so it can't be negative

Tests

Negative, large-negative and oversized headers, plus a well-formed response to show the normal path still reads its body. Removing the guard makes the first two fail with the original panic, so the tests genuinely cover it.

go test ./... is green.

How I found it

I was fuzzing the receiver-facing parsers — parseHTTPHeader, parseTXT, parseFeatures, parseCaptureFrames, parseXrandrGeometry — for about 11.8 million executions total. All clean; none of them crash. This one isn't reachable by fuzzing those functions in isolation, because the panic is at the use of the parsed value rather than in the parse. It turned up reading the allocation sites afterwards.

Happy to contribute the fuzz targets separately if you'd want them in the tree.

A receiver answering with "Content-Length: -1" crashes the sender. The
value is parsed with Sscanf into an int, is not checked, and reaches
make([]byte, contentLength) in readPlaintextHTTPResponse, which panics
with "makeslice: len out of range". Nothing recovers it.

Reproduced end to end over a net.Pipe standing in for the receiver: the
panic happens inside readPlaintextHTTPResponse, not in a helper.

Also bounds the positive case. Content-Length is attacker- or
bug-controlled and was used unbounded, so "Content-Length: 2147483647"
asks the sender for a 2 GB allocation before a single body byte arrives.
Control-channel bodies here are small plists; the 8 MB limit is far above
anything legitimate.

The other four make([]byte, n) sites in this package are fine and are
left alone -- readEncryptedFrame already bounds its uint16 length,
hkdfSHA512 and padTo take caller-supplied sizes, and audio.go's n comes
from a Read.

Tests cover negative, large-negative and oversized headers, plus a
well-formed response to show the normal path still works. Removing the
guard makes the first two fail with the original panic.
@omarroth

omarroth commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Great, thanks!

@omarroth
omarroth merged commit cdc039d into omarroth:main Aug 3, 2026
3 checks passed
objevovat added a commit to objevovat/fairplay-sap-core-airplay2-sender-authentication-handshake that referenced this pull request Aug 6, 2026
The reply body was allocated as make([]byte, clen) directly from the header,
with the parse error discarded. A receiver declaring 8 GB got 8 GB allocated
before a single byte of body was read; a non-numeric length silently became
zero, so a truncated exchange looked like an empty-bodied success; a negative
length was accepted.

This is the same bug class as omarroth/doubletake#24 — which this project
reported upstream and got merged, and then shipped itself. Writing the fix for
someone else's parser did not prevent it here, because rtsp/ had no tests at
all. It was found by reading the coverage report, not the code.

Fixed by parsing strictly and capping at 1 MB, which is three orders of
magnitude above anything this client exchanges (/info is a few hundred bytes,
an m2 is 142, an m4 is 32).

Adds the package's first tests: the three malformed-length cases, a 142-byte
round trip asserting the request carries X-Apple-ET and a correct CSeq, CSeq
incrementing across requests on one connection, malformed status lines, and
that 403/404/470 come back as inspectable responses rather than transport
errors — the device matrix depends on telling those apart.

rtsp coverage 0% -> 85.3%. Each malformed-length test was confirmed to fail
against the unfixed client first, and a real HomePod still accepts an m3 after
the change.
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.

2 participants