Skip to content

Commit 0a10f6d

Browse files
divinity76SakiTakamachi
authored andcommitted
ext/pdo_sqlite - throw on null bytes / resolve phpGH-13952 (php#18320)
fix a corruption issue where PDO::quote for SQLite would silently truncate strings with null bytes in them, by throwing. Fixes php#13952 Closes php#18320
1 parent 93ad8e8 commit 0a10f6d

File tree

4 files changed

+100
-1
lines changed

4 files changed

+100
-1
lines changed

NEWS

+3
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ PHP NEWS
112112
Pdo\Pgsql::prepare(…, [ PDO::ATTR_PREFETCH => 0 ]) make fetch() lazy
113113
instead of storing the whole result set in memory (Guillaume Outters)
114114

115+
- PDO_SQLITE:
116+
. throw on null bytes / resolve GH-13952 (divinity76).
117+
115118
- PGSQL:
116119
. Added pg_close_stmt to close a prepared statement while allowing
117120
its name to be reused. (David Carlier)

UPGRADING

+4
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,10 @@ PHP 8.5 UPGRADE NOTES
222222
PDO::ATTR_PREFETCH sets to 0 which set to lazy fetch mode.
223223
In this mode, statements cannot be run parallely.
224224

225+
- PDO_SQLITE:
226+
. SQLite PDO::quote() will now throw an exception or emit a warning,
227+
depending on the error mode, if the string contains a null byte.
228+
225229
- PGSQL:
226230
. pg_copy_from also supports inputs as Iterable.
227231
. pg_connect checks if the connection_string argument contains

ext/pdo_sqlite/sqlite_driver.c

+14-1
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,19 @@ 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+
230+
if (UNEXPECTED(zend_str_has_nul_byte(unquoted))) {
231+
if (dbh->error_mode == PDO_ERRMODE_EXCEPTION) {
232+
zend_throw_exception_ex(
233+
php_pdo_get_exception(), 0,
234+
"SQLite PDO::quote does not support null bytes");
235+
} else if (dbh->error_mode == PDO_ERRMODE_WARNING) {
236+
php_error_docref(NULL, E_WARNING,
237+
"SQLite PDO::quote does not support null bytes");
238+
}
239+
return NULL;
240+
}
241+
229242
quoted = safe_emalloc(2, ZSTR_LEN(unquoted), 3);
230243
/* TODO use %Q format? */
231244
sqlite3_snprintf(2*ZSTR_LEN(unquoted) + 3, quoted, "'%q'", ZSTR_VAL(unquoted));
@@ -741,7 +754,7 @@ static const struct pdo_dbh_methods sqlite_methods = {
741754
pdo_sqlite_request_shutdown,
742755
pdo_sqlite_in_transaction,
743756
pdo_sqlite_get_gc,
744-
pdo_sqlite_scanner
757+
pdo_sqlite_scanner
745758
};
746759

747760
static char *make_filename_safe(const char *filename)

ext/pdo_sqlite/tests/gh13952.phpt

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
--TEST--
2+
GH-13952 (sqlite PDO::quote handles null bytes correctly)
3+
--EXTENSIONS--
4+
pdo
5+
pdo_sqlite
6+
--FILE--
7+
<?php
8+
9+
$modes = [
10+
'exception' => PDO::ERRMODE_EXCEPTION,
11+
'warning' => PDO::ERRMODE_WARNING,
12+
'silent' => PDO::ERRMODE_SILENT,
13+
];
14+
15+
$test_cases = [
16+
"",
17+
"x",
18+
"\x00",
19+
"a\x00b",
20+
"\x00\x00\x00",
21+
"foobar",
22+
"foo'''bar",
23+
"'foo'''bar'",
24+
"foo\x00bar",
25+
"'foo'\x00'bar'",
26+
"foo\x00\x00\x00bar",
27+
"\x00foo\x00\x00\x00bar\x00",
28+
"\x00\x00\x00foo",
29+
"foo\x00\x00\x00",
30+
"\x80", // << invalid UTF-8
31+
"\x00\x80\x00", // << invalid UTF-8 with null bytes
32+
];
33+
34+
foreach ($modes as $mode_name => $mode) {
35+
echo "Testing error mode: $mode_name\n";
36+
$db = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => $mode]);
37+
38+
foreach ($test_cases as $test) {
39+
$contains_null = str_contains($test, "\x00");
40+
41+
if ($mode === PDO::ERRMODE_EXCEPTION && $contains_null) {
42+
set_error_handler(fn() => throw new PDOException(), E_WARNING);
43+
try {
44+
$db->quote($test);
45+
throw new LogicException("Expected exception not thrown.");
46+
} catch (PDOException) {
47+
// expected
48+
} finally {
49+
restore_error_handler();
50+
}
51+
} else {
52+
set_error_handler(fn() => null, E_WARNING);
53+
$quoted = $db->quote($test);
54+
restore_error_handler();
55+
56+
if ($contains_null) {
57+
if ($quoted !== false) {
58+
throw new LogicException("Expected false, got: " . var_export($quoted, true));
59+
}
60+
} else {
61+
if ($quoted === false) {
62+
throw new LogicException("Unexpected false from quote().");
63+
}
64+
$fetched = $db->query("SELECT $quoted")->fetchColumn();
65+
if ($fetched !== $test) {
66+
throw new LogicException("Data corrupted: expected " . var_export($test, true) . " got " . var_export($fetched, true));
67+
}
68+
}
69+
}
70+
}
71+
}
72+
73+
echo "ok\n";
74+
?>
75+
--EXPECT--
76+
Testing error mode: exception
77+
Testing error mode: warning
78+
Testing error mode: silent
79+
ok

0 commit comments

Comments
 (0)