1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

Implement GH-17321: Add setAuthorizer to Pdo\Sqlite (#17905)

This commit is contained in:
Niels Dossche
2025-04-18 00:34:46 +02:00
committed by GitHub
parent 87499e44f2
commit 8376904aeb
10 changed files with 328 additions and 16 deletions

1
NEWS
View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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 @@ class Sqlite extends \PDO
?string $dbname = "main",
int $flags = \Pdo\Sqlite::OPEN_READONLY
) {}
public function setAuthorizer(?callable $callback): void {}
}

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 7ceaf5fc8e9c92bf192e824084a706794395ce1a */
* Stub hash: f8cd6b3c6aa662d76dca3d0a28d61acfb5a611b5 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Pdo_Sqlite_createAggregate, 0, 3, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
@@ -34,6 +34,10 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Pdo_Sqlite_openBlob, 0, 0, 3)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "Pdo\\Sqlite::OPEN_READONLY")
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Pdo_Sqlite_setAuthorizer, 0, 1, IS_VOID, 0)
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 1)
ZEND_END_ARG_INFO()
ZEND_METHOD(Pdo_Sqlite, createAggregate);
ZEND_METHOD(Pdo_Sqlite, createCollation);
ZEND_METHOD(Pdo_Sqlite, createFunction);
@@ -41,6 +45,7 @@ ZEND_METHOD(Pdo_Sqlite, createFunction);
ZEND_METHOD(Pdo_Sqlite, loadExtension);
#endif
ZEND_METHOD(Pdo_Sqlite, openBlob);
ZEND_METHOD(Pdo_Sqlite, setAuthorizer);
static const zend_function_entry class_Pdo_Sqlite_methods[] = {
ZEND_ME(Pdo_Sqlite, createAggregate, arginfo_class_Pdo_Sqlite_createAggregate, ZEND_ACC_PUBLIC)
@@ -50,6 +55,7 @@ static const zend_function_entry class_Pdo_Sqlite_methods[] = {
ZEND_ME(Pdo_Sqlite, loadExtension, arginfo_class_Pdo_Sqlite_loadExtension, ZEND_ACC_PUBLIC)
#endif
ZEND_ME(Pdo_Sqlite, openBlob, arginfo_class_Pdo_Sqlite_openBlob, ZEND_ACC_PUBLIC)
ZEND_ME(Pdo_Sqlite, setAuthorizer, arginfo_class_Pdo_Sqlite_setAuthorizer, ZEND_ACC_PUBLIC)
ZEND_FE_END
};
@@ -104,5 +110,23 @@ static zend_class_entry *register_class_Pdo_Sqlite(zend_class_entry *class_entry
zend_declare_typed_class_constant(class_entry, const_ATTR_EXTENDED_RESULT_CODES_name, &const_ATTR_EXTENDED_RESULT_CODES_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_ATTR_EXTENDED_RESULT_CODES_name);
zval const_OK_value;
ZVAL_LONG(&const_OK_value, SQLITE_OK);
zend_string *const_OK_name = zend_string_init_interned("OK", sizeof("OK") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_OK_name, &const_OK_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_OK_name);
zval const_DENY_value;
ZVAL_LONG(&const_DENY_value, SQLITE_DENY);
zend_string *const_DENY_name = zend_string_init_interned("DENY", sizeof("DENY") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_DENY_name, &const_DENY_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_DENY_name);
zval const_IGNORE_value;
ZVAL_LONG(&const_IGNORE_value, SQLITE_IGNORE);
zend_string *const_IGNORE_name = zend_string_init_interned("IGNORE", sizeof("IGNORE") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_IGNORE_name, &const_IGNORE_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_IGNORE_name);
return class_entry;
}

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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