Skip to content

fix(auth): bound the fixed-width reads of the client authentication response - #5998

Merged
renecannao merged 5 commits into
v3.0from
fix/auth-response-length-guards
Aug 9, 2026
Merged

fix(auth): bound the fixed-width reads of the client authentication response#5998
renecannao merged 5 commits into
v3.0from
fix/auth-response-length-guards

Conversation

@renecannao

@renecannao renecannao commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

The client's authentication response is heap-allocated from a length the client declares:

unsigned char pass_len = pkt[cur];                        // 0..255, attacker-chosen
if ((size_t)(packet_end - pass_ptr) < pass_len) return false;
pass = (unsigned char *)malloc(pass_len + 1);

The bounds check only confirms the packet carries that many bytes. Nothing enforces a minimum.

Six comparisons then read a fixed width from that buffer. A client sending a 1-byte response gets a 2-byte allocation and ProxySQL reads up to 30 bytes past its end — before authentication has succeeded.

Impact is a crash where the allocation abuts an unmapped page, or heap corruption detectable under ASAN. memcmp leaks only equality, so this is a reliability/DoS issue rather than direct disclosure.

Sites

function read width
verify_user_pass() memcmp, native, cleartext-stored 20
verify_user_pass() proxy_scramble_sha1, hashed-stored 20
PPHR_5passwordFalse_0() memcmp, native, monitor credential 20
caching_sha2_fast_auth_verify() memcmp 32
PPHR_7auth1() proxy_scramble_sha1, hashed-stored 20
PPHR_verify_password() memcmp, native 20

The proxy_scramble_sha1() sites matter most: that helper feeds the response to proxy_my_crypt() for SCRAMBLE_LENGTH bytes, and both call sites are the hashed-password path — the common case, since stored passwords are normally *-prefixed.

Reach: the verify_user_pass / PPHR_* sites apply to any mysql_users account. The monitor sites additionally require the username to equal mysql-monitor_username (default monitor).

Fix

One helper, used at all six sites:

static inline bool auth_response_has(int64_t pass_len, size_t need) {
    return pass_len >= 0 && static_cast<uint64_t>(pass_len) + 1 >= need;
}

It tests against the allocation size (pass_len + 1), not an exact length.

Gating on pass_len == need would be wrong, and that is the trap here. PPHR_2 strips a trailing NUL from the response (remove the extra 0 if present), so a legitimate 20-byte native response ending in 0x00 — about 1 in 256 — arrives with pass_len == 19 while all 20 bytes are present. An equality gate rejects real logins intermittently; measured at 20 spurious denials across ~6520 connections in test_auth_methods-t. The rationale is recorded on the helper so it is not "tightened" later.

process_pkt_auth_swich_response() deliberately has no guard: len is validated to be exactly sizeof(mysql_hdr)+20 and the buffer is a zeroed 128-byte stack array. A comment records why.

Verification

test_auth_methods-t: 40194/40194 — all assertion numbers present, zero not ok anywhere in the stream, binary RC 0, on a PROXYSQL31 debug build.

That is a functional no-regression check. It does not exercise a short response — a raw-socket short-response case under ASAN would be the direct regression test and is not included here.

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened authentication response validation for supported password authentication methods.
    • Improved handling of short or unexpectedly sized authentication responses.
    • Added consistent validation for native-password and caching-SHA2 authentication.
    • Fixed credential verification for monitor authentication.
    • Improved compatibility with authentication responses containing a trailing null character.
    • Reduced the risk of authentication issues caused by malformed or oversized responses.

…esponse

The client's authentication response is heap-allocated from a length the CLIENT
declares:

    unsigned char pass_len = pkt[cur];                 // 0..255, attacker-chosen
    if ((size_t)(packet_end - pass_ptr) < pass_len) return false;
    pass = (unsigned char *)malloc(pass_len + 1);

The bounds check only confirms the packet carries that many bytes; nothing
enforces a MINIMUM. Six comparisons then read a fixed width from that buffer --
SHA_DIGEST_LENGTH (20), SCRAMBLE_LENGTH (20) or SHA256_DIGEST_LENGTH (32) -- so
a client sending a 1-byte response gets a 2-byte allocation that is read up to
30 bytes past its end, before authentication has succeeded.

Guarded sites (all reachable pre-auth):

  verify_user_pass()          memcmp native, cleartext-stored password   20
  verify_user_pass()          proxy_scramble_sha1, hashed-stored         20
  PPHR_5passwordFalse_0()     memcmp native, monitor credential          20
  caching_sha2_fast_auth_verify()                                        32
  PPHR_7auth1()               proxy_scramble_sha1, hashed-stored         20
  PPHR_verify_password()      memcmp native                              20

The proxy_scramble_sha1() sites matter most: that helper feeds the response to
proxy_my_crypt() for SCRAMBLE_LENGTH bytes, and both call sites are the
hashed-password path -- the common case, since stored passwords are normally
'*'-prefixed.

All six now route through one helper, auth_response_has(pass_len, need), which
tests 'pass_len + 1 >= need', i.e. the ALLOCATION size.

Gating on 'pass_len == need' instead would be wrong and is the trap here:
PPHR_2 strips a trailing NUL from the response ("remove the extra 0 if
present"), so a legitimate 20-byte native response ending in 0x00 -- about 1 in
256 -- arrives with pass_len == 19 while all 20 bytes are present. An equality
gate rejects real logins intermittently; when I tried it, test_auth_methods-t
showed 20 spurious denials across ~6520 connections. Comparing against the
allocation admits that case and still bounds the read. The rationale is recorded
on the helper so it is not "tightened" later.

process_pkt_auth_swich_response() needs no guard and did not get one: 'len' is
validated to be exactly sizeof(mysql_hdr)+20 and the buffer is a zeroed 128-byte
stack array. A comment now records why.

Verified: test_auth_methods-t passes 40194/40194 (all assertion numbers present,
zero 'not ok' anywhere in the stream, binary RC 0) on a PROXYSQL31 debug build.
That is a functional no-regression check; it does not exercise a short response.
A raw-socket short-response case under ASAN would be the direct regression test
and is not included here.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 833504af-0381-4956-877e-3c8ae3170800

📥 Commits

Reviewing files that changed from the base of the PR and between 6195d06 and 7268e51.

📒 Files selected for processing (3)
  • include/MySQL_Authentication.hpp
  • lib/MySQL_Authentication.cpp
  • lib/MySQL_Protocol.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/MySQL_Protocol.cpp
📜 Recent review details
⏰ Context from checks skipped due to timeout. (6)
  • GitHub Check: CI-builds / builds (ubuntu22,-tap-mysqlx)
  • GitHub Check: CI-builds / builds (debian12,-dbg)
  • GitHub Check: CI-builds / builds (ubuntu24,-tap-genai-gcov)
  • GitHub Check: CI-builds / builds (ubuntu22,-tap)
  • GitHub Check: run / trigger
  • GitHub Check: build
🧰 Additional context used
📓 Path-based instructions (2)
include/**/*.hpp

📄 CodeRabbit inference engine (CLAUDE.md)

Header include guards use the #ifndef __CLASS_*_H convention.

Files:

  • include/MySQL_Authentication.hpp
**/*.{cpp,h,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{cpp,h,hpp}: Class names must use PascalCase with protocol prefixes such as MySQL_, PgSQL_, and ProxySQL_.
Member variables must use snake_case.
Constants and macros must use UPPER_SNAKE_CASE.
Use C++17, and gate conditional code with #ifdef PROXYSQL31, #ifdef PROXYSQL40, #ifdef PROXYSQLFFTO, #ifdef PROXYSQLTSDB, and #ifdef PROXYSQLCLICKHOUSE; PROXYSQLGENAI must not guard core code outside plugins/genai/.
Consider performance implications when changing hot paths or other performance-critical code.
Use RAII for resource management and jemalloc for allocation.
Use pthread mutexes for synchronization and std::atomic<> for counters.

Files:

  • include/MySQL_Authentication.hpp
  • lib/MySQL_Authentication.cpp
🔇 Additional comments (2)
lib/MySQL_Authentication.cpp (1)

541-541: LGTM!

include/MySQL_Authentication.hpp (1)

104-104: 🗄️ Data Integrity & Integration

Confirm the binary boundary for set_SHA1.

Make this public signature change compatible with shipped consumers, or document and version the ABI break. Rebuilt callers can use the new const char * declaration, but existing binaries or separately built plugins referencing the old MySQL_Authentication::set_SHA1(char *, ...) symbol may fail to link; provide a compatibility overload or shim if this header is part of the exported API.


📝 Walkthrough

Walkthrough

Authentication verification now validates client response allocations before fixed-length SHA-1 and SHA-256 comparisons. Caching-SHA2 verification receives the client response length. The set_SHA1 username parameter is now read-only.

Changes

Authentication response validation

Layer / File(s) Summary
Authentication API const-correctness
include/MySQL_Authentication.hpp, lib/MySQL_Authentication.cpp
set_SHA1 now accepts a const char* username. Credential lookup behavior is unchanged.
Shared response-size checks
lib/MySQL_Protocol.cpp
The auth_response_has helper validates fixed-length response reads while allowing a stripped trailing NUL. Native-password and hashed-password SHA-1 checks apply these bounds. Auth-switch responses retain fixed-buffer handling.
Caching-SHA2 verification
lib/MySQL_Protocol.cpp
caching_sha2_fast_auth_verify receives the client response length and rejects allocations shorter than the 32-byte digest. Regular and monitor callers pass the length.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Poem

A rabbit checks each byte in line,
Short digests stop at the boundary sign.
SHA checks read only what is there,
A trailing NUL may disappear.
Read-only names keep the path precise. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: bounding fixed-width reads of client authentication responses.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/auth-response-length-guards

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

SonarCloud flagged two C-style casts removing const (cpp:M23_090, CRITICAL) in
verify_user_pass(). They are pre-existing lines that this branch does not modify
-- they were reported as new only because the added length guards shifted the
line numbers -- but they are in the function this PR touches and the fix is free.

set_SHA1() takes 'char*' and does not modify the username, so const_cast states
the intent explicitly instead of silently stripping const with a C-style cast.
No behaviour change.
cpp:M23_090 fires on ANY cast that removes const, including const_cast -- the
SonarCloud message after the change read 'const_cast removing const
qualification', with the same two CRITICAL findings and the same B maintainability
rating. The change achieved nothing, so it is reverted to keep this PR's diff to
the length guards.

The findings are pre-existing lines that this branch does not modify; they are
attributed to the PR only because the added guards shifted the line numbers. The
real fix is to make set_SHA1() take 'const char*' -- it only reads the username
(strlen + SpookyHash::Update) -- but that signature change ripples through
MySQL_Authentication, PgSQL_Authentication and ClickHouse_Authentication plus
their headers, which does not belong in a pre-auth security fix.
Brings the branch up to date with v3.0 (37 commits, including #5991 and the
#5999 salt-length/NUL fixes) and re-triggers a full CI run from a clean state.

Merge verified: lib/MySQL_Protocol.cpp auto-merged keeping all six
auth_response_has() bounds checks from this branch, with the reverted
set_SHA1 const_cast staying reverted; the CACHING_SHA2_PASSWORD() salt-length
and NUL rejections and the deterministic salt sweep come across from v3.0
intact. sqlite3 dep rebuilt against the merged patch; builds clean under
PROXYSQL31.
@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 14.28571% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.97%. Comparing base (6159730) to head (7268e51).
⚠️ Report is 36 commits behind head on v3.0.

Files with missing lines Patch % Lines
lib/MySQL_Protocol.cpp 10.00% 15 Missing and 3 partials ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             v3.0    #5998       +/-   ##
===========================================
+ Coverage   13.87%   52.97%   +39.10%     
===========================================
  Files         154      473      +319     
  Lines       82411   143386    +60975     
  Branches        0    36265    +36265     
===========================================
+ Hits        11431    75962    +64531     
+ Misses      70980    50572    -20408     
- Partials        0    16852    +16852     
Flag Coverage Δ
integration-tests 49.17% <9.52%> (?)
unit-tests 14.36% <4.76%> (+0.49%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

SonarCloud reported two CRITICAL cpp:M23_090 findings on this PR:

    C-style cast removing const qualification from the type of a pointer
    lib/MySQL_Protocol.cpp:1364, :1382

They are pre-existing lines this branch does not modify -- they surfaced only
because the added length guards shifted the line numbers -- but they sit in the
function this PR touches, so they are worth clearing properly.

An earlier attempt swapped the C-style casts for const_cast. That was useless:
cpp:M23_090 fires on ANY cast removing const, and the finding simply came back
reading "const_cast removing const qualification", with the same B maintainability
rating. It was reverted.

The actual fix is to stop removing const. set_SHA1() only reads the username --
strlen() plus SpookyHash::Update() -- so it can take 'const char*', and both call
sites then need no cast at all.

I had deferred this as a cross-class refactor. That was wrong: MySQL_Authentication
is standalone (PgSQL_Authentication declares its own set_SHA1 and ClickHouse's is
commented out), so the change is 4 lines across 3 files.

The remaining (char *) casts at :1289, :2298, :2410 and :2451 are untouched and
not flagged -- they convert unsigned char*/char*, they do not strip const.
@sonarqubecloud

sonarqubecloud Bot commented Aug 9, 2026

Copy link
Copy Markdown

@renecannao
renecannao merged commit 2bd3c17 into v3.0 Aug 9, 2026
79 of 81 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.

1 participant