Skip to content

proto: reject transport parameters with a mismatched length - #2832

Merged
djc merged 1 commit into
quinn-rs:mainfrom
MaxFreedomPollard:proto-tp-length-check
Sep 10, 2026
Merged

djc merged 1 commit into
quinn-rs:mainfrom
MaxFreedomPollard:proto-tp-length-check

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

TransportParameters::read reads the length field of every transport parameter, but three arms of its decoding loop never check that the value actually takes up that many bytes. In quinn-proto/src/transport_parameters.rs on main, PreferredAddress (line 457) decodes a fixed number of bytes for a given connection ID length, MaxDatagramFrameSize (line 469) only rejects a length above 8, and MinAckDelayDraft07 (line 479) checks nothing at all. Anything left over stays in the buffer, and the next turn of the loop decodes it as though it were the start of the next parameter. Every other arm does check, either directly or through decode_cid, and the macro arm rejects len != value.size().

The effect is that a peer can hide a parameter it never encoded. Send max_datagram_frame_size with a declared length of 3 and the value bytes 00 0c 00: the value decodes as the one-byte varint 0, and the trailing 0c 00 is a complete disable_active_migration. On main that byte string parses without error and comes back with max_datagram_frame_size: Some(0) and disable_active_migration: true. So the parameters quinn ends up acting on are not the ones the peer encoded.

RFC 9000 section 18 says the Transport Parameter Length field "contains the length of the Transport Parameter Value field in bytes", and section 7.4 asks for TRANSPORT_PARAMETER_ERROR on a parameter with an invalid value, which is what Error::Malformed maps to here.

Rather than patch the three arms one at a time, I record r.remaining() before the match and compare it once afterwards. That covers all three, and any arm added later gets the check for free. The new read_length_mismatch test builds a byte string for each of the three cases and expects Err(Error::Malformed).

Verification on macOS with rustc 1.95.0: cargo test --locked -p quinn-proto gives 303 passed, 0 failed, plus 3 doc-tests passing. cargo fmt --all -- --check and cargo clippy --locked -p quinn-proto --all-targets -- -D warnings are both clean. To confirm the test is really testing the fix I backed the length check out and left the test in: read_length_mismatch then fails on the first case, reporting the parsed parameters with disable_active_migration: true where it expected Err(Malformed).

@Ralith Ralith left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks!

Comment thread quinn-proto/src/transport_parameters.rs

@djc djc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks!

Please squash these commits.

And would be great if you're able to backport this to the 0.11.x branch, as well.

Comment thread quinn-proto/src/transport_parameters.rs Outdated
The decoding loop in TransportParameters::read reads each parameter's
length field, but three of its arms never check that the value really
occupies that many bytes. MaxDatagramFrameSize only rejects a length
above 8, MinAckDelayDraft07 checks nothing, and PreferredAddress decodes
a fixed number of bytes for a given connection ID length. Surplus bytes
stay in the buffer and the next turn of the loop decodes them as if they
were the next parameter.

A peer can use that to smuggle in a parameter it never encoded. Sending
max_datagram_frame_size with a length of 3 and the value bytes 00 0c 00
leaves 0c 00 behind, which is a complete disable_active_migration.

Compare the declared length against the number of bytes consumed once,
after the match, so the check covers every arm.

Cover each of the three arms with its own test: read_length_mismatch,
read_min_ack_delay_length_mismatch and
read_preferred_address_length_mismatch each build a byte string for
their case and expect Err(Error::Malformed).
@djc
djc added this pull request to the merge queue Sep 10, 2026
Merged via the queue into quinn-rs:main with commit 621e38a Sep 10, 2026
20 checks passed
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.

3 participants