-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Fix GH-13952: sqlite PDO::quote silently corrupts strings with null bytes #13956
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not suggesting it's better but alternatively we could convert the entire string to hex if it contains null chars. Pros: faster, simpler impl. Cons (in the all-hex case): more string memory, unreadable return value. Curious what you thought of those trade-offs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah the extra memory / IO bandwidth necessary for that case was the main argument against that. In particular, if we don't expect many NULL bytes then the current approach is better. |
||
return quoted_str; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,70 @@ | ||||||
--TEST-- | ||||||
GH-13952 (sqlite PDO::quote silently corrupts strings with null bytes) | ||||||
--EXTENSIONS-- | ||||||
pdo | ||||||
pdo_sqlite | ||||||
--FILE-- | ||||||
<?php | ||||||
$db = new \PDO('sqlite::memory:', null, null, array( | ||||||
\PDO::ATTR_ERRMODE => \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' | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
to make sure higher precedence operator (like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! |
||||||
\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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would
zend_mempcpy
be usable here ?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I can do that later today