Skip to content

Commit 565fe3a

Browse files
committed
Use x'hex' syntax for null bytes in pdo::quote
Implements x'hex' encoding in pdo::quote for handling strings with null bytes, providing a reliable workaround for issue phpGH-13952. An alternative fix is discussed in PR php#13956 PR php#13956 does something interesting, it avoids the overhead of copying to/from sqlite3_snprintf, probably speeding up PDO::quote, but right now I just want to keep things simple.
1 parent 8ce9f2e commit 565fe3a

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed

ext/pdo_sqlite/sqlite_driver.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,23 @@ static zend_string* sqlite_handle_quoter(pdo_dbh_t *dbh, const zend_string *unqu
226226
if (ZSTR_LEN(unquoted) > (INT_MAX - 3) / 2) {
227227
return NULL;
228228
}
229+
if(ZSTR_LEN(unquoted) != 0 && memchr(ZSTR_VAL(unquoted), '\0', ZSTR_LEN(unquoted))) {
230+
// x'hex'
231+
zend_string *quoted = zend_string_safe_alloc(3 + (2 * ZSTR_LEN(unquoted)), 1, 0, 0);
232+
char *outptr = ZSTR_VAL(quoted);
233+
const char *inptr = ZSTR_VAL(unquoted);
234+
const char *const inendptr = inptr + ZSTR_LEN(unquoted);
235+
*outptr++ = 'x';
236+
*outptr++ = '\'';
237+
while(inptr != inendptr) {
238+
const unsigned char c = *inptr++;
239+
*outptr++ = "0123456789ABCDEF"[c >> 4];
240+
*outptr++ = "0123456789ABCDEF"[c & 0x0F];
241+
}
242+
*outptr++ = '\'';
243+
*outptr = '\0'; // todo: does zend_string_safe_alloc do this for us? i don't know
244+
return quoted;
245+
}
229246
quoted = safe_emalloc(2, ZSTR_LEN(unquoted), 3);
230247
/* TODO use %Q format? */
231248
sqlite3_snprintf(2*ZSTR_LEN(unquoted) + 3, quoted, "'%q'", ZSTR_VAL(unquoted));

ext/pdo_sqlite/tests/gh13952.phpt

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
--TEST--
2+
GH-13952 (sqlite PDO::quote silently corrupts strings with null bytes)
3+
--EXTENSIONS--
4+
pdo
5+
pdo_sqlite
6+
--FILE--
7+
<?php
8+
$db = new \PDO('sqlite::memory:', null, null, array(
9+
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
10+
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
11+
\PDO::ATTR_EMULATE_PREPARES => false,
12+
));
13+
14+
$test_cases = [
15+
"",
16+
"x",
17+
"\x00",
18+
"a\x00b",
19+
"\x00\x00\x00",
20+
"foobar",
21+
"foo'''bar",
22+
"'foo'''bar'",
23+
"'foo'\x00'bar'",
24+
"foo\x00\x00\x00bar",
25+
"\x00foo\x00\x00\x00bar\x00",
26+
"\x00\x00\x00foo",
27+
"foo\x00\x00\x00",
28+
];
29+
30+
foreach($test_cases as $test){
31+
$res = $db->query("SELECT " . $db->quote($test))->fetch($db::FETCH_NUM)[0] === $test;
32+
if(!$res){
33+
throw new Exception("Failed for $test");
34+
}
35+
}
36+
37+
$db->exec('CREATE TABLE test (name TEXT)');
38+
39+
foreach ($test_cases as $test_case) {
40+
$quoted = $db->quote($test_case);
41+
echo trim(json_encode($test_case), '"'), " -> $quoted\n";
42+
$db->exec("INSERT INTO test (name) VALUES (" . $quoted . ")");
43+
}
44+
45+
$stmt = $db->prepare('SELECT * from test');
46+
$stmt->execute();
47+
foreach ($stmt->fetchAll() as $result) {
48+
var_dump($result['name']);
49+
}
50+
?>
51+
--EXPECTF--
52+
-> ''
53+
x -> 'x'
54+
\u0000 -> x'00'
55+
a\u0000b -> x'610062'
56+
\u0000\u0000\u0000 -> x'000000'
57+
foobar -> 'foobar'
58+
foo'''bar -> 'foo''''''bar'
59+
'foo'''bar' -> '''foo''''''bar'''
60+
'foo'\u0000'bar' -> x'27666F6F27002762617227'
61+
foo\u0000\u0000\u0000bar -> x'666F6F000000626172'
62+
\u0000foo\u0000\u0000\u0000bar\u0000 -> x'00666F6F00000062617200'
63+
\u0000\u0000\u0000foo -> x'000000666F6F'
64+
foo\u0000\u0000\u0000 -> x'666F6F000000'
65+
string(0) ""
66+
string(1) "x"
67+
string(1) "%0"
68+
string(3) "a%0b"
69+
string(3) "%0%0%0"
70+
string(6) "foobar"
71+
string(9) "foo'''bar"
72+
string(11) "'foo'''bar'"
73+
string(11) "'foo'%0'bar'"
74+
string(9) "foo%0%0%0bar"
75+
string(11) "%0foo%0%0%0bar%0"
76+
string(6) "%0%0%0foo"
77+
string(6) "foo%0%0%0"

0 commit comments

Comments
 (0)