Commit 8376904aeb6 for php.net
commit 8376904aeb636bc2a44dad4b712f1458ad0a65e1
Author: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Fri Apr 18 00:34:46 2025 +0200
Implement GH-17321: Add setAuthorizer to Pdo\Sqlite (#17905)
diff --git a/NEWS b/NEWS
index 38af4b7681d..80bda686a1b 100644
--- a/NEWS
+++ b/NEWS
@@ -114,6 +114,7 @@ PHP NEWS
- PDO_SQLITE:
. throw on null bytes / resolve GH-13952 (divinity76).
+ . Implement GH-17321: Add setAuthorizer to Pdo\Sqlite. (nielsdos)
- PGSQL:
. Added pg_close_stmt to close a prepared statement while allowing
diff --git a/UPGRADING b/UPGRADING
index 2b7abad173c..2ce2ba3c120 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -309,6 +309,11 @@ PHP 8.5 UPGRADE NOTES
. Added enchant_dict_remove() to put a word on the exclusion list and
remove it from the session dictionary.
+- Pdo\Sqlite:
+ . Added support for Pdo\Sqlite::setAuthorizer(), which is the equivalent of
+ SQLite3::setAuthorizer(). The only interface difference is that the
+ pdo version returns void.
+
- PGSQL:
. pg_close_stmt offers an alternative way to close a prepared
statement from the DEALLOCATE sql command in that we can reuse
diff --git a/ext/pdo_sqlite/pdo_sqlite.c b/ext/pdo_sqlite/pdo_sqlite.c
index ff56d040494..7661b36c5d3 100644
--- a/ext/pdo_sqlite/pdo_sqlite.c
+++ b/ext/pdo_sqlite/pdo_sqlite.c
@@ -332,6 +332,36 @@ PHP_METHOD(Pdo_Sqlite, openBlob)
}
}
+PHP_METHOD(Pdo_Sqlite, setAuthorizer)
+{
+ zend_fcall_info fci;
+ zend_fcall_info_cache fcc;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_FUNC_NO_TRAMPOLINE_FREE_OR_NULL(fci, fcc)
+ ZEND_PARSE_PARAMETERS_END();
+
+ pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS);
+ PDO_CONSTRUCT_CHECK_WITH_CLEANUP(free_fcc);
+ pdo_sqlite_db_handle *db_handle = (pdo_sqlite_db_handle *) dbh->driver_data;
+
+ /* Clear previously set callback */
+ if (ZEND_FCC_INITIALIZED(db_handle->authorizer_fcc)) {
+ zend_fcc_dtor(&db_handle->authorizer_fcc);
+ }
+
+ /* Only enable userland authorizer if argument is not NULL */
+ if (ZEND_FCI_INITIALIZED(fci)) {
+ zend_fcc_dup(&db_handle->authorizer_fcc, &fcc);
+ }
+
+ return;
+
+free_fcc:
+ zend_release_fcall_info_cache(&fcc);
+ RETURN_THROWS();
+}
+
static int php_sqlite_collation_callback(void *context, int string1_len, const void *string1,
int string2_len, const void *string2)
{
@@ -349,7 +379,7 @@ static int php_sqlite_collation_callback(void *context, int string1_len, const v
if (!Z_ISUNDEF(retval)) {
if (Z_TYPE(retval) != IS_LONG) {
zend_string *func_name = get_active_function_or_method_name();
- zend_type_error("%s(): Return value of the callback must be of type int, %s returned",
+ zend_type_error("%s(): Return value of the collation callback must be of type int, %s returned",
ZSTR_VAL(func_name), zend_zval_value_name(&retval));
zend_string_release(func_name);
zval_ptr_dtor(&retval);
diff --git a/ext/pdo_sqlite/pdo_sqlite.stub.php b/ext/pdo_sqlite/pdo_sqlite.stub.php
index e22c8ce10fc..3832683598e 100644
--- a/ext/pdo_sqlite/pdo_sqlite.stub.php
+++ b/ext/pdo_sqlite/pdo_sqlite.stub.php
@@ -33,6 +33,16 @@ class Sqlite extends \PDO
/** @cvalue PDO_SQLITE_ATTR_EXTENDED_RESULT_CODES */
public const int ATTR_EXTENDED_RESULT_CODES = UNKNOWN;
+ /** @cvalue SQLITE_OK */
+ public const int OK = UNKNOWN;
+
+ /* Constants for authorizer return */
+
+ /** @cvalue SQLITE_DENY */
+ public const int DENY = UNKNOWN;
+ /** @cvalue SQLITE_IGNORE */
+ public const int IGNORE = UNKNOWN;
+
// Registers an aggregating User Defined Function for use in SQL statements
public function createAggregate(
string $name,
@@ -63,4 +73,6 @@ public function openBlob(
?string $dbname = "main",
int $flags = \Pdo\Sqlite::OPEN_READONLY
) {}
+
+ public function setAuthorizer(?callable $callback): void {}
}
diff --git a/ext/pdo_sqlite/pdo_sqlite_arginfo.h b/ext/pdo_sqlite/pdo_sqlite_arginfo.h
index 4abbc0bb625..75de256e55c 100644
Binary files a/ext/pdo_sqlite/pdo_sqlite_arginfo.h and b/ext/pdo_sqlite/pdo_sqlite_arginfo.h differ
diff --git a/ext/pdo_sqlite/php_pdo_sqlite_int.h b/ext/pdo_sqlite/php_pdo_sqlite_int.h
index 08d5f877ad5..4a39781f85c 100644
--- a/ext/pdo_sqlite/php_pdo_sqlite_int.h
+++ b/ext/pdo_sqlite/php_pdo_sqlite_int.h
@@ -50,6 +50,7 @@ typedef struct {
pdo_sqlite_error_info einfo;
struct pdo_sqlite_func *funcs;
struct pdo_sqlite_collation *collations;
+ zend_fcall_info_cache authorizer_fcc;
} pdo_sqlite_db_handle;
typedef struct {
diff --git a/ext/pdo_sqlite/sqlite_driver.c b/ext/pdo_sqlite/sqlite_driver.c
index 8a880a3425f..ddf25d4965f 100644
--- a/ext/pdo_sqlite/sqlite_driver.c
+++ b/ext/pdo_sqlite/sqlite_driver.c
@@ -97,6 +97,10 @@ static void pdo_sqlite_cleanup_callbacks(pdo_sqlite_db_handle *H)
{
struct pdo_sqlite_func *func;
+ if (ZEND_FCC_INITIALIZED(H->authorizer_fcc)) {
+ zend_fcc_dtor(&H->authorizer_fcc);
+ }
+
while (H->funcs) {
func = H->funcs;
H->funcs = func->next;
@@ -714,6 +718,10 @@ static void pdo_sqlite_get_gc(pdo_dbh_t *dbh, zend_get_gc_buffer *gc_buffer)
{
pdo_sqlite_db_handle *H = dbh->driver_data;
+ if (ZEND_FCC_INITIALIZED(H->authorizer_fcc)) {
+ zend_get_gc_buffer_add_fcc(gc_buffer, &H->authorizer_fcc);
+ }
+
struct pdo_sqlite_func *func = H->funcs;
while (func) {
if (ZEND_FCC_INITIALIZED(func->func)) {
@@ -784,24 +792,77 @@ static char *make_filename_safe(const char *filename)
return estrdup(filename);
}
-static int authorizer(void *autharg, int access_type, const char *arg3, const char *arg4,
- const char *arg5, const char *arg6)
+#define ZVAL_NULLABLE_STRING(zv, str) do { \
+ zval *zv_ = zv; \
+ const char *str_ = str; \
+ if (str_) { \
+ ZVAL_STRING(zv_, str_); \
+ } else { \
+ ZVAL_NULL(zv_); \
+ } \
+} while (0)
+
+static int authorizer(void *autharg, int access_type, const char *arg1, const char *arg2,
+ const char *arg3, const char *arg4)
{
- char *filename;
- switch (access_type) {
- case SQLITE_ATTACH: {
- filename = make_filename_safe(arg3);
+ if (PG(open_basedir) && *PG(open_basedir)) {
+ if (access_type == SQLITE_ATTACH) {
+ char *filename = make_filename_safe(arg1);
if (!filename) {
return SQLITE_DENY;
}
efree(filename);
- return SQLITE_OK;
}
+ }
- default:
- /* access allowed */
- return SQLITE_OK;
+ pdo_sqlite_db_handle *db_obj = autharg;
+
+ /* fallback to access allowed if authorizer callback is not defined */
+ if (!ZEND_FCC_INITIALIZED(db_obj->authorizer_fcc)) {
+ return SQLITE_OK;
+ }
+
+ /* call userland authorizer callback, if set */
+ zval retval;
+ zval argv[5];
+
+ ZVAL_LONG(&argv[0], access_type);
+ ZVAL_NULLABLE_STRING(&argv[1], arg1);
+ ZVAL_NULLABLE_STRING(&argv[2], arg2);
+ ZVAL_NULLABLE_STRING(&argv[3], arg3);
+ ZVAL_NULLABLE_STRING(&argv[4], arg4);
+
+ int authreturn = SQLITE_DENY;
+
+ zend_call_known_fcc(&db_obj->authorizer_fcc, &retval, /* argc */ 5, argv, /* named_params */ NULL);
+ if (Z_ISUNDEF(retval)) {
+ ZEND_ASSERT(EG(exception));
+ } else {
+ if (Z_TYPE(retval) != IS_LONG) {
+ zend_string *func_name = get_active_function_or_method_name();
+ zend_type_error("%s(): Return value of the authorizer callback must be of type int, %s returned",
+ ZSTR_VAL(func_name), zend_zval_value_name(&retval));
+ zend_string_release(func_name);
+ } else {
+ authreturn = Z_LVAL(retval);
+
+ if (authreturn != SQLITE_OK && authreturn != SQLITE_IGNORE && authreturn != SQLITE_DENY) {
+ zend_string *func_name = get_active_function_or_method_name();
+ zend_value_error("%s(): Return value of the authorizer callback must be one of Pdo\\Sqlite::OK, Pdo\\Sqlite::DENY, or Pdo\\Sqlite::IGNORE",
+ ZSTR_VAL(func_name));
+ zend_string_release(func_name);
+ authreturn = SQLITE_DENY;
+ }
+ }
}
+
+ zval_ptr_dtor(&retval);
+ zval_ptr_dtor(&argv[1]);
+ zval_ptr_dtor(&argv[2]);
+ zval_ptr_dtor(&argv[3]);
+ zval_ptr_dtor(&argv[4]);
+
+ return authreturn;
}
static int pdo_sqlite_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{ */
@@ -843,9 +904,7 @@ static int pdo_sqlite_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{
goto cleanup;
}
- if (PG(open_basedir) && *PG(open_basedir)) {
- sqlite3_set_authorizer(H->db, authorizer, NULL);
- }
+ sqlite3_set_authorizer(H->db, authorizer, H);
if (driver_options) {
timeout = pdo_attr_lval(driver_options, PDO_ATTR_TIMEOUT, timeout);
diff --git a/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer.phpt b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer.phpt
new file mode 100644
index 00000000000..d1e9039ea1c
--- /dev/null
+++ b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer.phpt
@@ -0,0 +1,101 @@
+--TEST--
+Pdo\Sqlite user authorizer callback
+--EXTENSIONS--
+pdo_sqlite
+--FILE--
+<?php
+
+$db = new Pdo\Sqlite('sqlite::memory:');
+
+$db->setAuthorizer(function (int $action) {
+ if ($action == 21 /* SELECT */) {
+ return Pdo\Sqlite::OK;
+ }
+
+ return Pdo\Sqlite::DENY;
+});
+
+// This query should be accepted
+var_dump($db->query('SELECT 1;'));
+
+try {
+ // This one should fail
+ var_dump($db->exec('CREATE TABLE test (a, b);'));
+} catch (\Exception $e) {
+ echo $e->getMessage() . "\n";
+}
+
+// Test disabling the authorizer
+$db->setAuthorizer(null);
+
+// This should now succeed
+var_dump($db->exec('CREATE TABLE test (a); INSERT INTO test VALUES (42);'));
+var_dump($db->exec('SELECT a FROM test;'));
+
+// Test if we are getting the correct arguments
+$db->setAuthorizer(function (int $action) {
+ $constants = ["COPY", "CREATE_INDEX", "CREATE_TABLE", "CREATE_TEMP_INDEX", "CREATE_TEMP_TABLE", "CREATE_TEMP_TRIGGER", "CREATE_TEMP_VIEW", "CREATE_TRIGGER", "CREATE_VIEW", "DELETE", "DROP_INDEX", "DROP_TABLE", "DROP_TEMP_INDEX", "DROP_TEMP_TABLE", "DROP_TEMP_TRIGGER", "DROP_TEMP_VIEW", "DROP_TRIGGER", "DROP_VIEW", "INSERT", "PRAGMA", "READ", "SELECT", "TRANSACTION", "UPDATE"];
+
+ var_dump($constants[$action], implode(',', array_slice(func_get_args(), 1)));
+ return Pdo\Sqlite::OK;
+});
+
+var_dump($db->exec('SELECT * FROM test WHERE a = 42;'));
+var_dump($db->exec('DROP TABLE test;'));
+
+// Try to return something invalid from the authorizer
+$db->setAuthorizer(function () {
+ return 'FAIL';
+});
+
+try {
+ var_dump($db->query('SELECT 1;'));
+} catch (\Error $e) {
+ echo $e->getMessage() . "\n";
+}
+
+$db->setAuthorizer(function () {
+ return 4200;
+});
+
+try {
+ var_dump($db->query('SELECT 1;'));
+} catch (\Error $e) {
+ echo $e->getMessage() . "\n";
+}
+
+?>
+--EXPECTF--
+object(PDOStatement)#%d (1) {
+ ["queryString"]=>
+ string(9) "SELECT 1;"
+}
+SQLSTATE[HY000]: General error: 23 not authorized
+int(1)
+int(1)
+string(6) "SELECT"
+string(3) ",,,"
+string(4) "READ"
+string(12) "test,a,main,"
+string(4) "READ"
+string(12) "test,a,main,"
+int(1)
+string(6) "DELETE"
+string(20) "sqlite_master,,main,"
+string(10) "DROP_TABLE"
+string(11) "test,,main,"
+string(6) "DELETE"
+string(11) "test,,main,"
+string(6) "DELETE"
+string(20) "sqlite_master,,main,"
+string(4) "READ"
+string(28) "sqlite_master,tbl_name,main,"
+string(4) "READ"
+string(24) "sqlite_master,type,main,"
+string(6) "UPDATE"
+string(28) "sqlite_master,rootpage,main,"
+string(4) "READ"
+string(28) "sqlite_master,rootpage,main,"
+int(1)
+PDO::query(): Return value of the authorizer callback must be of type int, string returned
+PDO::query(): Return value of the authorizer callback must be one of Pdo\Sqlite::OK, Pdo\Sqlite::DENY, or Pdo\Sqlite::IGNORE
diff --git a/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline.phpt b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline.phpt
new file mode 100644
index 00000000000..c93a1f2e34a
--- /dev/null
+++ b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline.phpt
@@ -0,0 +1,43 @@
+--TEST--
+Pdo\Sqlite user authorizer trampoline callback
+--EXTENSIONS--
+pdo_sqlite
+--FILE--
+<?php
+
+class TrampolineTest {
+ public function __call(string $name, array $arguments) {
+ echo 'Trampoline for ', $name, PHP_EOL;
+ if ($arguments[0] == 21 /* SELECT */) {
+ return Pdo\Sqlite::OK;
+ }
+
+ return Pdo\Sqlite::DENY;
+ }
+}
+$o = new TrampolineTest();
+$callback = [$o, 'authorizer'];
+
+$db = new Pdo\Sqlite('sqlite::memory:');
+
+$db->setAuthorizer($callback);
+
+// This query should be accepted
+var_dump($db->query('SELECT 1;'));
+
+try {
+ // This one should fail
+ var_dump($db->query('CREATE TABLE test (a, b);'));
+} catch (\Exception $e) {
+ echo $e->getMessage() . "\n";
+}
+
+?>
+--EXPECTF--
+Trampoline for authorizer
+object(PDOStatement)#%d (1) {
+ ["queryString"]=>
+ string(9) "SELECT 1;"
+}
+Trampoline for authorizer
+SQLSTATE[HY000]: General error: 23 not authorized
diff --git a/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline_no_leak.phpt b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline_no_leak.phpt
new file mode 100644
index 00000000000..84b83877b94
--- /dev/null
+++ b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline_no_leak.phpt
@@ -0,0 +1,36 @@
+--TEST--
+PdoSqlite::setAuthorizer use F ZPP for trampoline callback and does not leak
+--EXTENSIONS--
+pdo_sqlite
+--FILE--
+<?php
+
+class TrampolineTest {
+ public function __call(string $name, array $arguments) {
+ echo 'Trampoline for ', $name, PHP_EOL;
+ if ($arguments[0] == Pdo\Sqlite::SELECT) {
+ return Pdo\Sqlite::OK;
+ }
+
+ return Pdo\Sqlite::DENY;
+ }
+}
+$o = new TrampolineTest();
+$callback = [$o, 'authorizer'];
+
+echo "Invalid Pdo\Sqlite object:\n";
+$rc = new ReflectionClass(Pdo\Sqlite::class);
+$obj = $rc->newInstanceWithoutConstructor();
+
+try {
+ var_dump($obj->setAuthorizer($callback));
+} catch (\Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+DONE
+--EXPECT--
+Invalid Pdo\Sqlite object:
+Error: Pdo\Sqlite object is uninitialized
+DONE