@@ -3593,4 +3593,113 @@ function ( $row ) {
35933593 // Verify autoincrement detection works with backtick-quoted identifiers.
35943594 $ this ->assertStringContainsString ( 'AUTO_INCREMENT ' , $ create_sql );
35953595 }
3596+
3597+ public function testDoubleQuotedStringsAreParameterized () {
3598+ $ this ->assertQuery ( 'INSERT INTO _options (option_name, option_value) VALUES ("dq_name", "dq_value") ' );
3599+
3600+ // The double-quoted strings should be bound as parameters, not inlined.
3601+ $ insert_query = null ;
3602+ foreach ( $ this ->engine ->executed_sqlite_queries as $ q ) {
3603+ if ( stripos ( $ q ['sql ' ], 'INSERT ' ) !== false && stripos ( $ q ['sql ' ], '_options ' ) !== false ) {
3604+ $ insert_query = $ q ;
3605+ }
3606+ }
3607+ $ this ->assertNotNull ( $ insert_query );
3608+ $ this ->assertNotEmpty ( $ insert_query ['params ' ], 'Double-quoted strings should be bound as parameters ' );
3609+ $ this ->assertStringNotContainsString ( 'dq_name ' , $ insert_query ['sql ' ], 'Value should not appear in SQL ' );
3610+ $ this ->assertStringNotContainsString ( 'dq_value ' , $ insert_query ['sql ' ], 'Value should not appear in SQL ' );
3611+ $ this ->assertContains ( 'dq_name ' , $ insert_query ['params ' ] );
3612+ $ this ->assertContains ( 'dq_value ' , $ insert_query ['params ' ] );
3613+
3614+ // Verify the data was inserted correctly.
3615+ $ result = $ this ->assertQuery ( 'SELECT * FROM _options WHERE option_name = "dq_name" ' );
3616+ $ this ->assertCount ( 1 , $ result );
3617+ $ this ->assertEquals ( 'dq_value ' , $ result [0 ]->option_value );
3618+ }
3619+
3620+ public function testDoubleQuotedStringWithBackslashEscapeDoesNotCauseInjection () {
3621+ // In MySQL, \" inside double-quoted strings is an escaped double quote.
3622+ // The MySQL lexer produces a single token: "admin\" OR 1=1--"
3623+ // with value: admin" OR 1=1--
3624+ //
3625+ // Without parameterization, passing the raw token to SQLite would be:
3626+ // "admin\" OR 1=1--" (SQLite sees "admin\" as identifier + SQL)
3627+ //
3628+ // With parameterization, the value is safely bound as a parameter.
3629+ $ this ->assertQuery (
3630+ 'INSERT INTO _options (option_name, option_value) VALUES ("safe_key", "admin\" OR 1=1--") '
3631+ );
3632+
3633+ // Verify the injection payload is not present in the SQL sent to SQLite.
3634+ $ insert_query = null ;
3635+ foreach ( $ this ->engine ->executed_sqlite_queries as $ q ) {
3636+ if ( stripos ( $ q ['sql ' ], 'INSERT ' ) !== false && stripos ( $ q ['sql ' ], '_options ' ) !== false ) {
3637+ $ insert_query = $ q ;
3638+ }
3639+ }
3640+ $ this ->assertNotNull ( $ insert_query );
3641+ $ this ->assertStringNotContainsString ( 'OR 1=1 ' , $ insert_query ['sql ' ], 'Injection payload should not appear in SQL ' );
3642+ $ this ->assertNotEmpty ( $ insert_query ['params ' ], 'Values should be bound as parameters ' );
3643+
3644+ $ result = $ this ->assertQuery ( 'SELECT * FROM _options WHERE option_name = "safe_key" ' );
3645+ $ this ->assertCount ( 1 , $ result );
3646+ $ this ->assertEquals ( 'admin" OR 1=1-- ' , $ result [0 ]->option_value );
3647+ }
3648+
3649+ public function testDateFormatWithSingleQuotesInFormat () {
3650+ $ this ->assertQuery (
3651+ 'CREATE TABLE _tmp_dates (
3652+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
3653+ created_at DATETIME NOT NULL
3654+ ); '
3655+ );
3656+ $ this ->assertQuery ( "INSERT INTO _tmp_dates (created_at) VALUES ('2024-01-15 10:30:00') " );
3657+
3658+ // DATE_FORMAT with a format that produces a value — verify it works.
3659+ $ result = $ this ->assertQuery (
3660+ "SELECT DATE_FORMAT(created_at, '%Y-%m-%d') as formatted FROM _tmp_dates "
3661+ );
3662+ $ this ->assertCount ( 1 , $ result );
3663+ $ this ->assertEquals ( '2024-01-15 ' , $ result [0 ]->formatted );
3664+ }
3665+
3666+ public function testIntervalExpression () {
3667+ $ this ->assertQuery (
3668+ 'CREATE TABLE _tmp_dates (
3669+ ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
3670+ created_at DATETIME NOT NULL
3671+ ); '
3672+ );
3673+ $ this ->assertQuery ( 'INSERT INTO _tmp_dates (created_at) VALUES ( \'2024-01-15 10:30:00 \') ' );
3674+
3675+ $ result = $ this ->assertQuery (
3676+ 'SELECT DATE_ADD(created_at, INTERVAL 1 DAY) as future_date FROM _tmp_dates '
3677+ );
3678+ $ this ->assertCount ( 1 , $ result );
3679+ $ this ->assertEquals ( '2024-01-16 10:30:00 ' , $ result [0 ]->future_date );
3680+
3681+ $ result = $ this ->assertQuery (
3682+ 'SELECT DATE_SUB(created_at, INTERVAL 1 DAY) as past_date FROM _tmp_dates '
3683+ );
3684+ $ this ->assertCount ( 1 , $ result );
3685+ $ this ->assertEquals ( '2024-01-14 10:30:00 ' , $ result [0 ]->past_date );
3686+ }
3687+
3688+ public function testLikeBinaryWithSingleQuoteInPattern () {
3689+ $ this ->assertQuery (
3690+ "CREATE TABLE _tmp_table (
3691+ ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
3692+ name varchar(50) NOT NULL default ''
3693+ ); "
3694+ );
3695+
3696+ $ this ->assertQuery ( "INSERT INTO _tmp_table (name) VALUES ('it''s a test') " );
3697+ $ this ->assertQuery ( "INSERT INTO _tmp_table (name) VALUES ('no quote here') " );
3698+
3699+ $ result = $ this ->assertQuery (
3700+ "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'it''s%' "
3701+ );
3702+ $ this ->assertCount ( 1 , $ result );
3703+ $ this ->assertEquals ( "it's a test " , $ result [0 ]->name );
3704+ }
35963705}
0 commit comments