Skip to content

Commit a03f7d5

Browse files
divinity76Niels Dossche
and
Niels Dossche
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 GH-13952. An alternative fix is discussed in PR #13956 PR #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. Co-authored-by: Niels Dossche <[email protected]>
1 parent 8ce9f2e commit a03f7d5

File tree

2 files changed

+101
-0
lines changed

2 files changed

+101
-0
lines changed

ext/pdo_sqlite/sqlite_driver.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,24 @@ 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+
// the odd (''||) thing is to make sure quote produce a sqlite datatype "string" rather than "blob" ...
232+
// https://github.com/php/php-src/pull/13962/files#r1565485792
233+
zend_string *quoted = zend_string_safe_alloc(9 + (2 * ZSTR_LEN(unquoted)), 1, 0, 0);
234+
char *outptr = ZSTR_VAL(quoted);
235+
const char *inptr = ZSTR_VAL(unquoted);
236+
const char *const inendptr = inptr + ZSTR_LEN(unquoted);
237+
memcpy(outptr, "(''||x'", 7);
238+
outptr += 7;
239+
while(inptr != inendptr) {
240+
const unsigned char c = *inptr++;
241+
*outptr++ = "0123456789ABCDEF"[c >> 4];
242+
*outptr++ = "0123456789ABCDEF"[c & 0x0F];
243+
}
244+
memcpy(outptr, "')", 3); // todo: does zend_string_safe_alloc write the null terminator? if it does, reduce this to 2
245+
return quoted;
246+
}
229247
quoted = safe_emalloc(2, ZSTR_LEN(unquoted), 3);
230248
/* TODO use %Q format? */
231249
sqlite3_snprintf(2*ZSTR_LEN(unquoted) + 3, quoted, "'%q'", ZSTR_VAL(unquoted));

ext/pdo_sqlite/tests/gh13952.phpt

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
"\x80", // << invalid UTF8
29+
"\x00\x80\x00", // << invalid UTF8
30+
];
31+
32+
foreach($test_cases as $test){
33+
$res = $db->query("SELECT " . $db->quote($test))->fetch($db::FETCH_NUM)[0] === $test;
34+
if(!$res){
35+
throw new Exception("Failed for $test");
36+
}
37+
}
38+
39+
$db->exec('CREATE TABLE test (name TEXT)');
40+
41+
foreach ($test_cases as $test_case) {
42+
$quoted = $db->quote($test_case);
43+
echo trim(json_encode($test_case, JSON_PARTIAL_OUTPUT_ON_ERROR), '"'), " -> $quoted\n";
44+
$db->exec("INSERT INTO test (name) VALUES (" . $quoted . ")");
45+
}
46+
47+
$stmt = $db->prepare('SELECT * from test');
48+
$stmt->execute();
49+
foreach ($stmt->fetchAll() as $result) {
50+
var_dump($result['name']);
51+
}
52+
?>
53+
--EXPECTF--
54+
-> ''
55+
x -> 'x'
56+
\u0000 -> (''||x'00')
57+
a\u0000b -> (''||x'610062')
58+
\u0000\u0000\u0000 -> (''||x'000000')
59+
foobar -> 'foobar'
60+
foo'''bar -> 'foo''''''bar'
61+
'foo'''bar' -> '''foo''''''bar'''
62+
'foo'\u0000'bar' -> (''||x'27666F6F27002762617227')
63+
foo\u0000\u0000\u0000bar -> (''||x'666F6F000000626172')
64+
\u0000foo\u0000\u0000\u0000bar\u0000 -> (''||x'00666F6F00000062617200')
65+
\u0000\u0000\u0000foo -> (''||x'000000666F6F')
66+
foo\u0000\u0000\u0000 -> (''||x'666F6F000000')
67+
null -> '€'
68+
null -> (''||x'008000')
69+
string(0) ""
70+
string(1) "x"
71+
string(1) "%0"
72+
string(3) "a%0b"
73+
string(3) "%0%0%0"
74+
string(6) "foobar"
75+
string(9) "foo'''bar"
76+
string(11) "'foo'''bar'"
77+
string(11) "'foo'%0'bar'"
78+
string(9) "foo%0%0%0bar"
79+
string(11) "%0foo%0%0%0bar%0"
80+
string(6) "%0%0%0foo"
81+
string(6) "foo%0%0%0"
82+
string(1) "€"
83+
string(3) "%0€%0"

0 commit comments

Comments
 (0)