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