From c63f53e9a87063362272c886bb8e51e39e2a1712 Mon Sep 17 00:00:00 2001 From: Jeroen Date: Mon, 19 Oct 2026 15:09:17 +0200 Subject: [PATCH] Fix GH-23956: Regression with MySQL 4.1 after mysqlnd security fix The fix for GHSA-r6x9-5r99-36j7 made 1-byte EOF and auth switch packets fail the length check after reading the first byte. MySQL 4.1 sends such packets. Only require more data in the error branch, which actually reads it. Co-Authored-By: Claude Opus 5.5 --- ext/mysqli/tests/fake_server.inc | 30 ++++++++++++++++++++ ext/mysqli/tests/gh23956.phpt | 45 ++++++++++++++++++++++++++++++ ext/mysqli/tests/gh23956_2.phpt | 33 ++++++++++++++++++++++ ext/mysqlnd/mysqlnd_wireprotocol.c | 4 +-- 4 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 ext/mysqli/tests/gh23956.phpt create mode 100644 ext/mysqli/tests/gh23956_2.phpt diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc index 2a4d17f946e8..f36b1b2a8374 100644 --- a/ext/mysqli/tests/fake_server.inc +++ b/ext/mysqli/tests/fake_server.inc @@ -1445,6 +1445,36 @@ function my_mysqli_test_stmt_response_field_len_premature(my_mysqli_fake_server_ $conn->read(65536); } +function my_mysqli_test_query_response_eof_one_byte(my_mysqli_fake_server_conn $conn): void +{ + $rh = $conn->packet_generator->server_query_execute_data_response('strval'); + // MySQL 4.1 can send a 1-byte EOF packet (no warning count and status) after the metadata + $rh[3]->packet_length = '010000'; + $rh[3]->warnings = ''; + $rh[3]->server_status = ''; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + $conn->read(); + $conn->send($conn->packets_to_bytes($rh), "Query Response [1-byte EOF packet]"); + $conn->read(65536); +} + +function my_mysqli_test_auth_response_old_password(my_mysqli_fake_server_conn $conn): void +{ + $p = new my_mysqli_fake_packet(); + $p->packet_length = "010000"; + $p->packet_number = "02"; + // MySQL 4.1+ sends a single 0xFE byte when the account still uses an old password hash + $p->response_code = "fe"; + + $conn->send_server_greetings(); + $conn->read(); + $conn->send($p->to_bytes(), "Auth Response [old password]"); + $conn->read(65536); +} + function run_fake_server(string $test_function, int|string $port = 0): void { $host = '127.0.0.1'; diff --git a/ext/mysqli/tests/gh23956.phpt b/ext/mysqli/tests/gh23956.phpt new file mode 100644 index 000000000000..a54295b432dd --- /dev/null +++ b/ext/mysqli/tests/gh23956.phpt @@ -0,0 +1,45 @@ +--TEST-- +GH-23956 (Regression with MySQL 4.1 after mysqlnd security fix - 1-byte EOF packet) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +$conn = new mysqli($servername, $username, $password, "", $process->getPort()); + +echo "[*] Query the fake server...\n"; +$result = $conn->query("SELECT strval, strval FROM data"); + +while ($row = $result->fetch_row()) { + var_dump($row); +} +$conn->close(); + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: %s +[*] Received: %s +[*] Sending - Server OK: %s +[*] Query the fake server... +[*] Received: %s +[*] Sending - Query Response [1-byte EOF packet]: %s +array(2) { + [0]=> + string(4) "test" + [1]=> + string(4) "test" +} +done! diff --git a/ext/mysqli/tests/gh23956_2.phpt b/ext/mysqli/tests/gh23956_2.phpt new file mode 100644 index 000000000000..9bb35f088ce5 --- /dev/null +++ b/ext/mysqli/tests/gh23956_2.phpt @@ -0,0 +1,33 @@ +--TEST-- +GH-23956 (Regression with MySQL 4.1 after mysqlnd security fix - 1-byte old password auth response) +--EXTENSIONS-- +mysqli +--FILE-- +wait(); + +try { + new mysqli($servername, $username, $password, "", $process->getPort()); +} catch (mysqli_sql_exception $e) { + echo $e->getMessage(), "\n"; +} + +$process->terminate(); + +print "done!"; +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: %s +[*] Received: %s +[*] Sending - Auth Response [old password]: 01000002fe +mysqlnd cannot connect to MySQL 4.1+ using the old insecure authentication.%s +done! diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.c b/ext/mysqlnd/mysqlnd_wireprotocol.c index 495539d970df..17bad10c2aaf 100644 --- a/ext/mysqlnd/mysqlnd_wireprotocol.c +++ b/ext/mysqlnd/mysqlnd_wireprotocol.c @@ -740,9 +740,9 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet) /* Should be always 0x0 or ERROR_MARKER for error */ packet->response_code = uint1korr(p); p++; - BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->response_code) { + BAIL_IF_NOT_ENOUGH_DATA; php_mysqlnd_read_error_from_line(p, packet->header.size - 1, packet->error, sizeof(packet->error), &packet->error_no, packet->sqlstate @@ -1015,9 +1015,9 @@ php_mysqlnd_eof_read(MYSQLND_CONN_DATA * conn, void * _packet) /* Should be always EODATA_MARKER */ packet->field_count = uint1korr(p); p++; - BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->field_count) { + BAIL_IF_NOT_ENOUGH_DATA; php_mysqlnd_read_error_from_line(p, packet->header.size - 1, packet->error, sizeof(packet->error), &packet->error_no, packet->sqlstate