Skip to content

MQTT: Reject malformed no-payload packets with non-zero Remaining Length#16852

Merged
chrisvest merged 3 commits into
netty:4.1from
daguimu:fix/16851-mqtt-malformed-no-payload
Jun 1, 2026
Merged

MQTT: Reject malformed no-payload packets with non-zero Remaining Length#16852
chrisvest merged 3 commits into
netty:4.1from
daguimu:fix/16851-mqtt-malformed-no-payload

Conversation

@daguimu

@daguimu daguimu commented May 27, 2026

Copy link
Copy Markdown
Contributor

Motivation:

MqttDecoder.decodePayload's default branch returns null for payload-less message types (PINGREQ, PINGRESP, DISCONNECT, AUTH, CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP) without checking that bytesRemainingInVariablePart is 0. A fixed header that claims a non-zero Remaining Length for one of these types is silently accepted, and the leftover bytes are interpreted as another packet.

The reporter's exact repro from #16851 is C0 02 D0 00: a PINGREQ with Remaining Length 2 (invalid per MQTT 3.1.1 and MQTT 5.0, which both require Remaining Length 0 for PINGREQ) is currently decoded as a valid PINGREQ followed by a valid PINGRESP.

Modification:

Call validateNoBytesRemain(0) at the start of the default branch. The helper — already used by the SUBSCRIBE and UNSUBSCRIBE payload decoders — subtracts 0 from bytesRemainingInVariablePart and throws DecoderException when the result is non-zero, surfacing a single invalid message instead of letting leftover bytes leak into the next decode pass.

Added MqttCodecTest#testPingReqWithNonZeroRemainingLengthIsRejected that feeds the reporter's exact byte sequence and verifies a single failed message is produced. Confirmed it fails on the unfixed code (expected: <true> but was: <false>) and passes with the fix; all 112 codec-mqtt tests pass locally.

Result:

Malformed payload-less MQTT frames are flagged as a single invalid message rather than allowed to silently spill into the next packet. The fix applies uniformly to every message type that goes through the default branch.

Fixes #16851.

Motivation:

MqttDecoder.decodePayload's default branch returns null for payload-less message types (PINGREQ, PINGRESP, DISCONNECT, AUTH, CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP) without checking that bytesRemainingInVariablePart is 0. A fixed header that claims a non-zero Remaining Length for one of these types is silently accepted, and the leftover bytes are interpreted as another packet. The reporter's exact repro from netty#16851 is C0 02 D0 00: a PINGREQ with Remaining Length 2 (invalid per MQTT 3.1.1 and MQTT 5.0, which both require Remaining Length 0 for PINGREQ) is currently decoded as a valid PINGREQ followed by a valid PINGRESP.

Modification:

Call validateNoBytesRemain(0) at the start of the default branch. The helper, already used by the SUBSCRIBE and UNSUBSCRIBE payload decoders, subtracts 0 from bytesRemainingInVariablePart and throws DecoderException when the result is non-zero, surfacing a single invalidMessage instead of letting leftover bytes leak into the next decode pass.

Added MqttCodecTest#testPingReqWithNonZeroRemainingLengthIsRejected that feeds the reporter's exact byte sequence and verifies one failed message is produced. Confirmed it fails on the unfixed code (expected <true> but was <false>) and passes with the fix; all 112 codec-mqtt tests pass.

Result:

Malformed payload-less MQTT frames are flagged as a single invalid message rather than allowed to silently spill into the next packet. The fix applies uniformly to every message type that goes through the default branch.

Fixes netty#16851.
Comment thread codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttCodecTest.java Outdated
Comment thread codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttCodecTest.java Outdated
@normanmaurer normanmaurer added this to the 4.2.15.Final milestone May 27, 2026
@normanmaurer normanmaurer added needs-cherry-pick-4.2 This PR should be cherry-picked to 4.2 once merged. needs-cherry-pick-5.0 This PR should be cherry-picked to 5.0 once merged. labels May 27, 2026
Co-authored-by: Norman Maurer <norman_maurer@apple.com>

@chrisvest chrisvest 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.

One nit but otherwise looks good

Comment thread codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttDecoder.java Outdated
@chrisvest chrisvest enabled auto-merge (squash) June 1, 2026 17:07
@chrisvest chrisvest modified the milestones: 4.2.15.Final, 4.1.135.Final Jun 1, 2026
@chrisvest chrisvest disabled auto-merge June 1, 2026 22:38
@chrisvest chrisvest merged commit d89cf88 into netty:4.1 Jun 1, 2026
9 of 18 checks passed
@netty-project-bot

Copy link
Copy Markdown
Contributor

Auto-port PR for 5.0: #16889

@netty-project-bot

Copy link
Copy Markdown
Contributor

Auto-port PR for 4.2: #16890

@github-actions github-actions Bot removed the needs-cherry-pick-4.2 This PR should be cherry-picked to 4.2 once merged. label Jun 1, 2026
chrisvest added a commit that referenced this pull request Jun 2, 2026
…o Remaining Length (#16890)

Auto-port of #16852 to 4.2
Cherry-picked commit: d89cf88

---
Motivation:

`MqttDecoder.decodePayload`'s `default` branch returns `null` for
payload-less message types (`PINGREQ`, `PINGRESP`, `DISCONNECT`, `AUTH`,
`CONNACK`, `PUBACK`, `PUBREC`, `PUBREL`, `PUBCOMP`) without checking
that `bytesRemainingInVariablePart` is `0`. A fixed header that claims a
non-zero Remaining Length for one of these types is silently accepted,
and the leftover bytes are interpreted as another packet.

The reporter's exact repro from #16851 is `C0 02 D0 00`: a `PINGREQ`
with Remaining Length `2` (invalid per [MQTT
3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)
and [MQTT
5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html),
which both require Remaining Length `0` for `PINGREQ`) is currently
decoded as a valid `PINGREQ` followed by a valid `PINGRESP`.

Modification:

Call `validateNoBytesRemain(0)` at the start of the `default` branch.
The helper — already used by the `SUBSCRIBE` and `UNSUBSCRIBE` payload
decoders — subtracts `0` from `bytesRemainingInVariablePart` and throws
`DecoderException` when the result is non-zero, surfacing a single
invalid message instead of letting leftover bytes leak into the next
decode pass.

Added `MqttCodecTest#testPingReqWithNonZeroRemainingLengthIsRejected`
that feeds the reporter's exact byte sequence and verifies a single
failed message is produced. Confirmed it fails on the unfixed code
(`expected: <true> but was: <false>`) and passes with the fix; all 112
`codec-mqtt` tests pass locally.

Result:

Malformed payload-less MQTT frames are flagged as a single invalid
message rather than allowed to silently spill into the next packet. The
fix applies uniformly to every message type that goes through the
`default` branch.

Fixes #16851.

Co-authored-by: Guimu <30684111+daguimu@users.noreply.github.com>
Co-authored-by: Norman Maurer <norman_maurer@apple.com>
Co-authored-by: Chris Vest <christianvest_hansen@apple.com>
chrisvest added a commit that referenced this pull request Jun 4, 2026
…o Remaining Length (#16889)

Auto-port of #16852 to 5.0
Cherry-picked commit: d89cf88

---
Motivation:

`MqttDecoder.decodePayload`'s `default` branch returns `null` for
payload-less message types (`PINGREQ`, `PINGRESP`, `DISCONNECT`, `AUTH`,
`CONNACK`, `PUBACK`, `PUBREC`, `PUBREL`, `PUBCOMP`) without checking
that `bytesRemainingInVariablePart` is `0`. A fixed header that claims a
non-zero Remaining Length for one of these types is silently accepted,
and the leftover bytes are interpreted as another packet.

The reporter's exact repro from #16851 is `C0 02 D0 00`: a `PINGREQ`
with Remaining Length `2` (invalid per [MQTT
3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)
and [MQTT
5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html),
which both require Remaining Length `0` for `PINGREQ`) is currently
decoded as a valid `PINGREQ` followed by a valid `PINGRESP`.

Modification:

Call `validateNoBytesRemain(0)` at the start of the `default` branch.
The helper — already used by the `SUBSCRIBE` and `UNSUBSCRIBE` payload
decoders — subtracts `0` from `bytesRemainingInVariablePart` and throws
`DecoderException` when the result is non-zero, surfacing a single
invalid message instead of letting leftover bytes leak into the next
decode pass.

Added `MqttCodecTest#testPingReqWithNonZeroRemainingLengthIsRejected`
that feeds the reporter's exact byte sequence and verifies a single
failed message is produced. Confirmed it fails on the unfixed code
(`expected: <true> but was: <false>`) and passes with the fix; all 112
`codec-mqtt` tests pass locally.

Result:

Malformed payload-less MQTT frames are flagged as a single invalid
message rather than allowed to silently spill into the next packet. The
fix applies uniformly to every message type that goes through the
`default` branch.

Fixes #16851.

---------

Co-authored-by: Guimu <30684111+daguimu@users.noreply.github.com>
Co-authored-by: Norman Maurer <norman_maurer@apple.com>
Co-authored-by: Chris Vest <christianvest_hansen@apple.com>
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.

4 participants