Skip to content

fix(mysql): harden protocol decoders against malformed packets from hostile servers - #4458

Merged
abonander merged 1 commit into
transact-rs:mainfrom
portlandhodl:security/mysql-decoder-hardening
Oct 16, 2026
Merged

abonander merged 1 commit into
transact-rs:mainfrom
portlandhodl:security/mysql-decoder-hardening

Conversation

@portlandhodl

Copy link
Copy Markdown
Contributor

Found during a security review of server-controlled parsing paths (threat model: rogue or MITM MySQL/MariaDB server; handshake parsing is reachable pre-authentication).

Does your PR solve an issue?

No tracking issue — reported here directly. This extends the fix pattern of #4176 (truncated OK packet) to the rest of the protocol; #4451 documents the same review on the PostgreSQL side.

The problem

  • recv_packet's continuation loop accumulated an unbounded number of 0xFFFFFF-sized parts: a hostile server streams endless 16 MiB frames and the client OOMs at near-zero bandwidth cost.
  • The result-set column count (a length-encoded u64) fed HashMap::with_capacity / Vec::reserve directly; a ~10-byte packet claiming 2^40 columns aborts the process.
  • Dozens of decode sites call panicking bytes::Buf getters / slice indexing / advance on untrusted buffers: truncated ERR packets, every field of HandshakeV10 (including the attacker-controlled auth-plugin-data length), length-encoded strings claiming more than the remaining bytes, TextRow field-count mismatch, BinaryRow (null bitmap, temporal length byte, fixed-width and lenenc value bounds — plus an unreachable!() actually reachable via a NULL-typed column), short EOF packets, and a 1-byte caching_sha2_password continue packet. The OkPacket decoder was hardened in fix(mysql): return error instead of panic on truncated OK packet #4176; the rest of the protocol was not.

The change

  • Cumulative 1 GiB cap on packet reassembly (MySql's own max_allowed_packet hard ceiling); protocol error beyond it.
  • Result sets claiming more than 4096 columns (the MySQL server hard limit) are protocol errors before any allocation.
  • All listed sites check remaining length up front and return Error::Protocol describing expectation vs. reality; the unreachable!() is now an error.

Is this a breaking change?

No: every changed path converts a panic or abort into a returned Err, which callers already handle (the pool discards the connection). Newly-observable edges: aggregate packets over 1 GiB and result sets over 4096 columns are now errors rather than allocation attempts.

Regression tests

15 new unit tests, including a sweep asserting that every strict prefix of captured MySQL 8.0.18 and MariaDB 10.4.7 handshake packets now errors instead of panicking. Each test fails (panics) without the fix. cargo test -p sqlx-mysql --lib passes.

…ostile servers

Found during a security review of server-controlled parsing paths (threat model: rogue or MITM MySQL/MariaDB server; handshake parsing is reachable pre-authentication).

### Does your PR solve an issue?

No tracking issue — reported here directly. This extends the fix pattern of transact-rs#4176 (truncated `OK` packet) to the rest of the protocol; transact-rs#4451 documents the same review on the PostgreSQL side.

### The problem

- `recv_packet`'s continuation loop accumulated an unbounded number of `0xFFFFFF`-sized parts: a hostile server streams endless 16 MiB frames and the client OOMs at near-zero bandwidth cost.
- The result-set column count (a length-encoded u64) fed `HashMap::with_capacity` / `Vec::reserve` directly; a ~10-byte packet claiming 2^40 columns aborts the process.
- Dozens of decode sites call panicking `bytes::Buf` getters / slice indexing / `advance` on untrusted buffers: truncated ERR packets, every field of `HandshakeV10` (including the attacker-controlled auth-plugin-data length), length-encoded strings claiming more than the remaining bytes, `TextRow` field-count mismatch, `BinaryRow` (null bitmap, temporal length byte, fixed-width and lenenc value bounds — plus an `unreachable!()` actually reachable via a NULL-typed column), short EOF packets, and a 1-byte `caching_sha2_password` continue packet. The OkPacket decoder was hardened in transact-rs#4176; the rest of the protocol was not.

### The change

- Cumulative 1 GiB cap on packet reassembly (`MySql`'s own `max_allowed_packet` hard ceiling); protocol error beyond it.
- Result sets claiming more than 4096 columns (the MySQL server hard limit) are protocol errors before any allocation.
- All listed sites check remaining length up front and return `Error::Protocol` describing expectation vs. reality; the `unreachable!()` is now an error.

### Is this a breaking change?

No: every changed path converts a panic or abort into a returned `Err`, which callers already handle (the pool discards the connection). Newly-observable edges: aggregate packets over 1 GiB and result sets over 4096 columns are now errors rather than allocation attempts.

### Regression tests

15 new unit tests, including a sweep asserting that every strict prefix of captured MySQL 8.0.18 and MariaDB 10.4.7 handshake packets now errors instead of panicking. Each test fails (panics) without the fix. `cargo test -p sqlx-mysql --lib` passes.
@portlandhodl
portlandhodl force-pushed the security/mysql-decoder-hardening branch from 97f2b8f to 404304e Compare October 14, 2026 04:34
@abonander
abonander merged commit 4161768 into transact-rs:main Oct 16, 2026
147 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.

2 participants