Describe the bug
pcapkit/protocols/application/httpv2.py:223 guards on if schema.length < 9: — the declared length read off the wire, never len(buffer). So a truncated frame whose declared length happens to be ≥ 9 parses, and the parsed object reports a length the capture does not contain. Measured on 0419c1c97, a 4-octet buffer sweeping the declared length:
declared=0 -> ProtocolError
declared=5 -> ProtocolError
declared=8 -> ProtocolError
declared=9 -> PARSED version=2 length=9
declared=10 -> ProtocolError
declared=15 -> PARSED version=2 length=15
declared=100 -> ProtocolError
declared=65535 -> PARSED version=2 length=65535
declared=16777215 -> PARSED version=2 length=16777215
A 4-octet buffer yields length=16777215. The value is attacker-controlled, and anything downstream that trusts length for framing or offsets is handed a number the capture cannot support.
Note the result is non-monotone — 9 parses, 10 refuses, 15 parses. A guard testing the intended quantity could not produce that shape; it is the signature of an accident rather than a design.
Expected behavior
The sub-9 class should be uniformly refused. Adding a buffer-length condition alongside the declared-length one — so both must hold — makes every truncated frame a ProtocolError.
Why this is worth more than it looks
It is the root cause of the only compromise #789 had to accept. That PR made _guess_version's HTTP/2 arm reachable and had to widen the last arm's suppression to (ProtocolError, struct.error), admitting in a code comment that a genuine httpv2 schema defect on well-formed HTTP/2 bytes would now surface as unknown HTTP version rather than crashing loudly.
That residual exists only because this guard tests the wrong quantity. Make the sub-9 class uniformly ProtocolError and _guess_version can drop the struct.error suppression entirely — so this is a follow-up that shrinks #789's surface rather than merely noting it.
It also became reachable from ordinary extraction at #789's head: on base the guess path died in arm 1, so via UDP/80 a truncated HTTP/2 frame became Raw; now it parses as a confident HTTP/2.
Additional context
Found by the cross-review on #789, which ranked it above the related residual that HTTPv2 constructed directly still raises a bare struct.error under 9 octets — the same defect from a less consequential angle. Both routes measured: HTTPv2 direct and HTTP(..., version=2) parse a 4-octet buffer declaring 15; HTTP() refused it on base and parses it at 11bb7693a.
Also related, and worth checking in the same pass: httpv2.py:292's make() emits _make_http_length(...) + 9, treating Length as the total frame size where RFC 9113 §4.1 says it is the payload size, header excluded. That is tracked separately as the round-trip asymmetry noted on #682's investigation; whoever fixes the guard should confirm the two are consistent afterwards.
Related: #789, #787, #682.
Describe the bug
pcapkit/protocols/application/httpv2.py:223guards onif schema.length < 9:— the declared length read off the wire, neverlen(buffer). So a truncated frame whose declared length happens to be ≥ 9 parses, and the parsed object reports a length the capture does not contain. Measured on0419c1c97, a 4-octet buffer sweeping the declared length:A 4-octet buffer yields
length=16777215. The value is attacker-controlled, and anything downstream that trustslengthfor framing or offsets is handed a number the capture cannot support.Note the result is non-monotone — 9 parses, 10 refuses, 15 parses. A guard testing the intended quantity could not produce that shape; it is the signature of an accident rather than a design.
Expected behavior
The sub-9 class should be uniformly refused. Adding a buffer-length condition alongside the declared-length one — so both must hold — makes every truncated frame a
ProtocolError.Why this is worth more than it looks
It is the root cause of the only compromise #789 had to accept. That PR made
_guess_version's HTTP/2 arm reachable and had to widen the last arm's suppression to(ProtocolError, struct.error), admitting in a code comment that a genuinehttpv2schema defect on well-formed HTTP/2 bytes would now surface asunknown HTTP versionrather than crashing loudly.That residual exists only because this guard tests the wrong quantity. Make the sub-9 class uniformly
ProtocolErrorand_guess_versioncan drop thestruct.errorsuppression entirely — so this is a follow-up that shrinks #789's surface rather than merely noting it.It also became reachable from ordinary extraction at #789's head: on base the guess path died in arm 1, so via UDP/80 a truncated HTTP/2 frame became
Raw; now it parses as a confidentHTTP/2.Additional context
Found by the cross-review on #789, which ranked it above the related residual that
HTTPv2constructed directly still raises a barestruct.errorunder 9 octets — the same defect from a less consequential angle. Both routes measured:HTTPv2direct andHTTP(..., version=2)parse a 4-octet buffer declaring 15;HTTP()refused it on base and parses it at11bb7693a.Also related, and worth checking in the same pass:
httpv2.py:292'smake()emits_make_http_length(...) + 9, treatingLengthas the total frame size where RFC 9113 §4.1 says it is the payload size, header excluded. That is tracked separately as the round-trip asymmetry noted on #682's investigation; whoever fixes the guard should confirm the two are consistent afterwards.Related: #789, #787, #682.