Skip to content

Commit 040d5a6

Browse files
committed
Add tests for read_quoted_text() bounds checking
Add tests to cover the backslash-counting loop in read_quoted_text(). These test cases expose an out-of-bounds string access triggered by unclosed quoted strings with trailing backslashes. Test cases: - Unclosed strings with odd/even trailing backslashes (single/double quotes) - Regression tests for valid escaped strings, doubled quotes, and backticks
1 parent de8f9b0 commit 040d5a6

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Lexer_Tests.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,73 @@ public function data_identifier_or_number(): array {
258258
);
259259
}
260260

261+
/**
262+
* Test that unclosed quoted strings with trailing backslashes do not
263+
* cause out-of-bounds string access in read_quoted_text().
264+
*
265+
* The backslash-counting loop walks backward from the position returned
266+
* by strcspn(). When the closing quote is missing, strcspn() returns
267+
* the remaining string length. If the last byte is a backslash, the
268+
* loop treats the absent quote as escaped and advances past the end
269+
* of the string. On the next iteration, the loop accesses an invalid
270+
* string offset, triggering "Uninitialized string offset" warnings.
271+
*
272+
* @dataProvider data_unclosed_strings_with_backslashes
273+
*/
274+
public function test_unclosed_string_with_trailing_backslash( string $sql ): void {
275+
set_error_handler(
276+
function ( $severity, $message, $file, $line ) {
277+
throw new \ErrorException( $message, 0, $severity, $file, $line );
278+
},
279+
E_WARNING | E_NOTICE
280+
);
281+
282+
try {
283+
$lexer = new WP_MySQL_Lexer( $sql );
284+
while ( $lexer->next_token() ) {
285+
// Consume all tokens.
286+
}
287+
} finally {
288+
restore_error_handler();
289+
}
290+
291+
// If we reach here without an ErrorException, no OOB access occurred.
292+
$this->assertNull( $lexer->get_token() );
293+
}
294+
295+
public function data_unclosed_strings_with_backslashes(): array {
296+
return array(
297+
'single-quoted trailing backslash' => array( "SELECT '\\" ),
298+
'double-quoted trailing backslash' => array( 'SELECT "\\' ),
299+
'even trailing backslashes' => array( "SELECT '\\\\" ),
300+
'odd trailing backslashes' => array( "SELECT '\\\\\\" ),
301+
'backslash-only single-quoted' => array( "'\\" ),
302+
'backslash-only double-quoted' => array( '"\\' ),
303+
);
304+
}
305+
306+
/**
307+
* Regression: valid strings with escapes must still tokenize correctly.
308+
*
309+
* @dataProvider data_valid_escaped_strings
310+
*/
311+
public function test_valid_escaped_string( string $sql, int $expected_token_id ): void {
312+
$lexer = new WP_MySQL_Lexer( $sql );
313+
$this->assertTrue( $lexer->next_token() );
314+
$this->assertSame( $expected_token_id, $lexer->get_token()->id );
315+
}
316+
317+
public function data_valid_escaped_strings(): array {
318+
return array(
319+
'escaped single quote' => array( "'it\\'s'", WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ),
320+
'trailing escaped backslash' => array( "'path\\\\'", WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ),
321+
'doubled single quote' => array( "'it''s'", WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ),
322+
'empty single-quoted string' => array( "''", WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ),
323+
'escaped double quote' => array( '"col\\"name"', WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ),
324+
'backtick identifier' => array( '`my_column`', WP_MySQL_Lexer::BACK_TICK_QUOTED_ID ),
325+
);
326+
}
327+
261328
private function get_token_names( array $token_types ): array {
262329
return array_map(
263330
function ( $token_type ) {

0 commit comments

Comments
 (0)