diff --git a/ext/pdo_sqlite/sqlite_driver.c b/ext/pdo_sqlite/sqlite_driver.c index 7cd8880b86099..70614b8771657 100644 --- a/ext/pdo_sqlite/sqlite_driver.c +++ b/ext/pdo_sqlite/sqlite_driver.c @@ -219,18 +219,96 @@ static zend_string *pdo_sqlite_last_insert_id(pdo_dbh_t *dbh, const zend_string return zend_i64_to_str(sqlite3_last_insert_rowid(H->db)); } -/* NB: doesn't handle binary strings... use prepared stmts for that */ +/* This does what "etSQLESCAPE2" does in sqlite3's sqlite3_snprintf, but binary safe, and with NULL escaping. */ static zend_string* sqlite_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquoted, enum pdo_param_type paramtype) { - char *quoted; - if (ZSTR_LEN(unquoted) > (INT_MAX - 3) / 2) { - return NULL; + static const char null_state_enter_at_start[] = "x'"; + static const char null_state_enter[] = "'||x'"; + static const char null_state_leave[] = "'||'"; + const size_t largest_addition = sizeof(null_state_enter) - 1 + sizeof(null_state_leave) - 1 + 2; + + bool state_in_nulls = false; + + /* First pass to compute necessary length */ + size_t quoted_len = 2; /* Two single quotes */ + const char *source = ZSTR_VAL(unquoted); + const char *source_end = source + ZSTR_LEN(unquoted); + while (source < source_end) { + /* If the largest addition could ever be larger than the maximum size of a string, bail. */ + if (UNEXPECTED(quoted_len >= ZSTR_MAX_LEN - largest_addition)) { + return NULL; + } + + if (*source == '\0') { + if (!state_in_nulls) { + state_in_nulls = true; + if (source == ZSTR_VAL(unquoted)) { + quoted_len--; /* backup initial ' */ + quoted_len += sizeof(null_state_enter_at_start) - 1; + } else { + quoted_len += sizeof(null_state_enter) - 1; + } + /* Every null state will eventually get back to the normal state. */ + quoted_len += sizeof(null_state_leave) - 1; + } + quoted_len += 2; /* '00' */ + } else { + if (*source == '\'') { + quoted_len += 2; /* Two single quotes */ + } else { + quoted_len++; + } + state_in_nulls = false; + } + source++; + } + + if (state_in_nulls) { + /* We don't emit the leave state if it ends in NULLs. */ + quoted_len -= sizeof(null_state_leave) - 1; + } + + /* Second pass to perform the transformation */ + zend_string *quoted_str = zend_string_alloc(quoted_len, false); + char *quoted_dest = ZSTR_VAL(quoted_str); + state_in_nulls = false; + + *quoted_dest++ = '\''; + + source = ZSTR_VAL(unquoted); + while (source < source_end) { + if (*source == '\0') { + if (!state_in_nulls) { + state_in_nulls = true; + if (source == ZSTR_VAL(unquoted)) { + quoted_dest--; /* backup initial ' */ + memcpy(quoted_dest, null_state_enter_at_start, sizeof(null_state_enter_at_start) - 1); + quoted_dest += sizeof(null_state_enter_at_start) - 1; + } else { + memcpy(quoted_dest, null_state_enter, sizeof(null_state_enter) - 1); + quoted_dest += sizeof(null_state_enter) - 1; + } + } + *quoted_dest++ = '0'; + *quoted_dest++ = '0'; + } else { + if (state_in_nulls) { + state_in_nulls = false; + memcpy(quoted_dest, null_state_leave, sizeof(null_state_leave) - 1); + quoted_dest += sizeof(null_state_leave) - 1; + } + if (*source == '\'') { + *quoted_dest++ = '\''; + } + *quoted_dest++ = *source; + state_in_nulls = false; + } + source++; } - quoted = safe_emalloc(2, ZSTR_LEN(unquoted), 3); - /* TODO use %Q format? */ - sqlite3_snprintf(2*ZSTR_LEN(unquoted) + 3, quoted, "'%q'", ZSTR_VAL(unquoted)); - zend_string *quoted_str = zend_string_init(quoted, strlen(quoted), 0); - efree(quoted); + + *quoted_dest++ = '\''; + *quoted_dest = '\0'; + return quoted_str; } diff --git a/ext/pdo_sqlite/tests/bug81740.phpt b/ext/pdo_sqlite/tests/bug81740.phpt index 2b8b9447f0fed..57bc979a44cad 100644 --- a/ext/pdo_sqlite/tests/bug81740.phpt +++ b/ext/pdo_sqlite/tests/bug81740.phpt @@ -14,7 +14,12 @@ memory_limit=-1 quote($string)); +$output = $pdo->quote($string); +var_dump(strlen($output) === 0x80000002); +var_dump($output[0] === "'"); +var_dump($output[0x80000001] === "'"); ?> --EXPECT-- -bool(false) +bool(true) +bool(true) +bool(true) diff --git a/ext/pdo_sqlite/tests/gh13952.phpt b/ext/pdo_sqlite/tests/gh13952.phpt new file mode 100644 index 0000000000000..47bb03b57524a --- /dev/null +++ b/ext/pdo_sqlite/tests/gh13952.phpt @@ -0,0 +1,70 @@ +--TEST-- +GH-13952 (sqlite PDO::quote silently corrupts strings with null bytes) +--EXTENSIONS-- +pdo +pdo_sqlite +--FILE-- + \PDO::ERRMODE_EXCEPTION, + \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, + \PDO::ATTR_EMULATE_PREPARES => false, +)); + +$test_cases = [ + "", + "x", + "\x00", + "a\x00b", + "\x00\x00\x00", + "foobar", + "foo'''bar", + "'foo'''bar'", + "'foo'\x00'bar'", + "foo\x00\x00\x00bar", + "\x00foo\x00\x00\x00bar\x00", + "\x00\x00\x00foo", + "foo\x00\x00\x00", +]; + +$db->exec('CREATE TABLE test (name TEXT)'); + +foreach ($test_cases as $test_case) { + $quoted = $db->quote($test_case); + echo trim(json_encode($test_case), '"'), " -> $quoted\n"; + $db->exec("INSERT INTO test (name) VALUES (" . $quoted . ")"); +} + +$stmt = $db->prepare('SELECT * from test'); +$stmt->execute(); +foreach ($stmt->fetchAll() as $result) { + var_dump($result['name']); +} +?> +--EXPECTF-- +-> '' +x -> 'x' +\u0000 -> x'00' +a\u0000b -> 'a'||x'00'||'b' +\u0000\u0000\u0000 -> x'000000' +foobar -> 'foobar' +foo'''bar -> 'foo''''''bar' +'foo'''bar' -> '''foo''''''bar''' +'foo'\u0000'bar' -> '''foo'''||x'00'||'''bar''' +foo\u0000\u0000\u0000bar -> 'foo'||x'000000'||'bar' +\u0000foo\u0000\u0000\u0000bar\u0000 -> x'00'||'foo'||x'000000'||'bar'||x'00' +\u0000\u0000\u0000foo -> x'000000'||'foo' +foo\u0000\u0000\u0000 -> 'foo'||x'000000' +string(0) "" +string(1) "x" +string(1) "%0" +string(3) "a%0b" +string(3) "%0%0%0" +string(6) "foobar" +string(9) "foo'''bar" +string(11) "'foo'''bar'" +string(11) "'foo'%0'bar'" +string(9) "foo%0%0%0bar" +string(11) "%0foo%0%0%0bar%0" +string(6) "%0%0%0foo" +string(6) "foo%0%0%0"