Add support for 'directonly' and 'innocuous' flags for user-defined functions
This commit is contained in:
parent
a9621bb301
commit
d80daaf387
|
@ -343,7 +343,7 @@ Connection Objects
|
|||
:meth:`~Cursor.executescript` method with the given *sql_script*, and
|
||||
returns the cursor.
|
||||
|
||||
.. method:: create_function(name, num_params, func, *, deterministic=False)
|
||||
.. method:: create_function(name, num_params, func, *, deterministic=False, directonly=False, innocous=False)
|
||||
|
||||
Creates a user-defined function that you can later use from within SQL
|
||||
statements under the function name *name*. *num_params* is the number of
|
||||
|
@ -355,18 +355,40 @@ Connection Objects
|
|||
SQLite 3.8.3 or higher, :exc:`NotSupportedError` will be raised if used
|
||||
with older versions.
|
||||
|
||||
The *innocuous* flag means that the function is unlikely to cause problems
|
||||
even if misused. An innocuous function should have no side effects and
|
||||
should not depend on any values other than its input parameters.
|
||||
Developers are advised to avoid using the *innocuous* flag for
|
||||
application-defined functions unless the function has been carefully
|
||||
audited and found to be free of potentially security-adverse side-effects
|
||||
and information-leaks. This flag is supported by SQLite 3.31.0 or higher.
|
||||
:exc:`NotSupportedError` will be raised if used with older SQLite versions.
|
||||
|
||||
The *directonly* flag means that the function may only be invoked from
|
||||
top-level SQL, and cannot be used in VIEWs or TRIGGERs nor in schema
|
||||
structures such as CHECK constraints, DEFAULT clauses, expression indexes,
|
||||
partial indexes, or generated columns. The *directonly* flag is a security
|
||||
feature which is recommended for all application-defined SQL functions,
|
||||
and especially for functions that have side-effects or that could
|
||||
potentially leak sensitive information. This flag is supported by SQLite
|
||||
3.31.0 or higher. :exc:`NotSupportedError` will be raised if used with
|
||||
older SQLite versions.
|
||||
|
||||
The function can return any of the types supported by SQLite: bytes, str, int,
|
||||
float and ``None``.
|
||||
|
||||
.. versionchanged:: 3.8
|
||||
The *deterministic* parameter was added.
|
||||
|
||||
.. versionchanged:: 3.10
|
||||
The *innocuous* and *directonly* parameters were added.
|
||||
|
||||
Example:
|
||||
|
||||
.. literalinclude:: ../includes/sqlite3/md5func.py
|
||||
|
||||
|
||||
.. method:: create_aggregate(name, num_params, aggregate_class)
|
||||
.. method:: create_aggregate(name, num_params, aggregate_class, directonly=False, innocuous=False)
|
||||
|
||||
Creates a user-defined aggregate function.
|
||||
|
||||
|
@ -378,6 +400,12 @@ Connection Objects
|
|||
The ``finalize`` method can return any of the types supported by SQLite:
|
||||
bytes, str, int, float and ``None``.
|
||||
|
||||
See :func:`create_function` for a description of the *innocuous* and
|
||||
*directonly* parameters.
|
||||
|
||||
.. versionchanged:: 3.10
|
||||
The *innocuous* and *directonly* parameters were added.
|
||||
|
||||
Example:
|
||||
|
||||
.. literalinclude:: ../includes/sqlite3/mysumaggr.py
|
||||
|
|
|
@ -316,6 +316,39 @@ class FunctionTests(unittest.TestCase):
|
|||
with self.assertRaises(TypeError):
|
||||
self.con.create_function("deterministic", 0, int, True)
|
||||
|
||||
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or higher")
|
||||
def CheckFuncNonInnocuousInTrustedEnv(self):
|
||||
mock = unittest.mock.Mock(return_value=None)
|
||||
self.con.create_function("noninnocuous", 0, mock, innocuous=False)
|
||||
self.con.execute("pragma trusted_schema = 0")
|
||||
self.con.execute("drop view if exists notallowed")
|
||||
self.con.execute("create view notallowed as select noninnocuous() = noninnocuous()")
|
||||
with self.assertRaises(sqlite.OperationalError) as cm:
|
||||
self.con.execute("select * from notallowed")
|
||||
self.assertEqual(str(cm.exception), 'unsafe use of noninnocuous()')
|
||||
|
||||
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or higher")
|
||||
def CheckFuncInnocuousInTrustedEnv(self):
|
||||
mock = unittest.mock.Mock(return_value=None)
|
||||
self.con.create_function("innocuous", 0, mock, innocuous=True)
|
||||
self.con.execute("pragma trusted_schema = 0")
|
||||
self.con.execute("drop view if exists allowed")
|
||||
self.con.execute("create view allowed as select innocuous() = innocuous()")
|
||||
self.con.execute("select * from allowed")
|
||||
self.assertEqual(mock.call_count, 2)
|
||||
|
||||
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or higher")
|
||||
def CheckFuncDirectOnly(self):
|
||||
mock = unittest.mock.Mock(return_value=None)
|
||||
self.con.create_function("directonly", 0, mock, directonly=True)
|
||||
self.con.execute("pragma trusted_schema = 1")
|
||||
self.con.execute("drop view if exists notallowed")
|
||||
self.con.execute("select directonly() = directonly()")
|
||||
self.assertEqual(mock.call_count, 2)
|
||||
self.con.execute("create view notallowed as select directonly() = directonly()")
|
||||
with self.assertRaises(sqlite.OperationalError) as cm:
|
||||
self.con.execute("select * from notallowed")
|
||||
self.assertEqual(str(cm.exception), 'unsafe use of directonly()')
|
||||
|
||||
class AggregateTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
@ -341,6 +374,9 @@ class AggregateTests(unittest.TestCase):
|
|||
self.con.create_aggregate("checkType", 2, AggrCheckType)
|
||||
self.con.create_aggregate("checkTypes", -1, AggrCheckTypes)
|
||||
self.con.create_aggregate("mysum", 1, AggrSum)
|
||||
if sqlite.sqlite_version_info >= (3, 31, 0):
|
||||
self.con.create_aggregate("mysumInnocuous", 1, AggrSum, innocuous=True)
|
||||
self.con.create_aggregate("mysumDirectOnly", 1, AggrSum, directonly=True)
|
||||
|
||||
def tearDown(self):
|
||||
#self.cur.close()
|
||||
|
@ -429,6 +465,45 @@ class AggregateTests(unittest.TestCase):
|
|||
val = cur.fetchone()[0]
|
||||
self.assertEqual(val, 60)
|
||||
|
||||
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or newer")
|
||||
def CheckAggrNonInnocuous(self):
|
||||
cur = self.con.cursor()
|
||||
cur.execute("pragma trusted_schema = 0")
|
||||
cur.execute("delete from test")
|
||||
cur.execute("drop view if exists notallowed")
|
||||
cur.execute("insert into test(i) values (?)", (10,))
|
||||
cur.execute("create view notallowed as select mysum(i) from test")
|
||||
with self.assertRaises(sqlite.OperationalError) as cm:
|
||||
cur.execute("select * from notallowed")
|
||||
self.assertEqual(str(cm.exception), 'unsafe use of mysum()')
|
||||
|
||||
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or newer")
|
||||
def CheckAggrInnocuous(self):
|
||||
cur = self.con.cursor()
|
||||
cur.execute("pragma trusted_schema = 0")
|
||||
cur.execute("delete from test")
|
||||
cur.execute("drop view if exists allowed")
|
||||
cur.executemany("insert into test(i) values (?)", [(10,), (20,), (30,)])
|
||||
cur.execute("create view allowed as select mysumInnocuous(i) from test")
|
||||
cur.execute("select * from allowed")
|
||||
val = cur.fetchone()[0]
|
||||
self.assertEqual(val, 60)
|
||||
|
||||
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or newer")
|
||||
def CheckAggrDirectOnly(self):
|
||||
cur = self.con.cursor()
|
||||
cur.execute("pragma trusted_schema = 1")
|
||||
cur.execute("delete from test")
|
||||
cur.execute("drop view if exists notallowed")
|
||||
cur.executemany("insert into test(i) values (?)", [(10,), (20,), (30,)])
|
||||
cur.execute("create view notallowed as select mysumDirectOnly(i) from test")
|
||||
with self.assertRaises(sqlite.OperationalError) as cm:
|
||||
cur.execute("select * from notallowed")
|
||||
self.assertEqual(str(cm.exception), 'unsafe use of mysumdirectonly()')
|
||||
cur.execute("select mysumDirectOnly(i) from test")
|
||||
val = cur.fetchone()[0]
|
||||
self.assertEqual(val, 60)
|
||||
|
||||
class AuthorizerTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def authorizer_cb(action, arg1, arg2, dbname, source):
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Add support for `SQLITE_INNOCUOUS` and `SQLITE_DIRECTONLY` flags in
|
||||
:mod:`sqlite3`.
|
|
@ -94,7 +94,8 @@ pysqlite_connection_rollback(pysqlite_Connection *self, PyObject *Py_UNUSED(igno
|
|||
}
|
||||
|
||||
PyDoc_STRVAR(pysqlite_connection_create_function__doc__,
|
||||
"create_function($self, /, name, narg, func, *, deterministic=False)\n"
|
||||
"create_function($self, /, name, narg, func, *, deterministic=False,\n"
|
||||
" directonly=False, innocuous=False)\n"
|
||||
"--\n"
|
||||
"\n"
|
||||
"Creates a new function. Non-standard.");
|
||||
|
@ -105,20 +106,23 @@ PyDoc_STRVAR(pysqlite_connection_create_function__doc__,
|
|||
static PyObject *
|
||||
pysqlite_connection_create_function_impl(pysqlite_Connection *self,
|
||||
const char *name, int narg,
|
||||
PyObject *func, int deterministic);
|
||||
PyObject *func, int deterministic,
|
||||
int directonly, int innocuous);
|
||||
|
||||
static PyObject *
|
||||
pysqlite_connection_create_function(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
|
||||
{
|
||||
PyObject *return_value = NULL;
|
||||
static const char * const _keywords[] = {"name", "narg", "func", "deterministic", NULL};
|
||||
static const char * const _keywords[] = {"name", "narg", "func", "deterministic", "directonly", "innocuous", NULL};
|
||||
static _PyArg_Parser _parser = {NULL, _keywords, "create_function", 0};
|
||||
PyObject *argsbuf[4];
|
||||
PyObject *argsbuf[6];
|
||||
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 3;
|
||||
const char *name;
|
||||
int narg;
|
||||
PyObject *func;
|
||||
int deterministic = 0;
|
||||
int directonly = 0;
|
||||
int innocuous = 0;
|
||||
|
||||
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 3, 3, 0, argsbuf);
|
||||
if (!args) {
|
||||
|
@ -145,19 +149,38 @@ pysqlite_connection_create_function(pysqlite_Connection *self, PyObject *const *
|
|||
if (!noptargs) {
|
||||
goto skip_optional_kwonly;
|
||||
}
|
||||
deterministic = PyObject_IsTrue(args[3]);
|
||||
if (deterministic < 0) {
|
||||
if (args[3]) {
|
||||
deterministic = PyObject_IsTrue(args[3]);
|
||||
if (deterministic < 0) {
|
||||
goto exit;
|
||||
}
|
||||
if (!--noptargs) {
|
||||
goto skip_optional_kwonly;
|
||||
}
|
||||
}
|
||||
if (args[4]) {
|
||||
directonly = PyObject_IsTrue(args[4]);
|
||||
if (directonly < 0) {
|
||||
goto exit;
|
||||
}
|
||||
if (!--noptargs) {
|
||||
goto skip_optional_kwonly;
|
||||
}
|
||||
}
|
||||
innocuous = PyObject_IsTrue(args[5]);
|
||||
if (innocuous < 0) {
|
||||
goto exit;
|
||||
}
|
||||
skip_optional_kwonly:
|
||||
return_value = pysqlite_connection_create_function_impl(self, name, narg, func, deterministic);
|
||||
return_value = pysqlite_connection_create_function_impl(self, name, narg, func, deterministic, directonly, innocuous);
|
||||
|
||||
exit:
|
||||
return return_value;
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(pysqlite_connection_create_aggregate__doc__,
|
||||
"create_aggregate($self, /, name, n_arg, aggregate_class)\n"
|
||||
"create_aggregate($self, /, name, n_arg, aggregate_class, *,\n"
|
||||
" deterministic=False, directonly=False, innocuous=False)\n"
|
||||
"--\n"
|
||||
"\n"
|
||||
"Creates a new aggregate. Non-standard.");
|
||||
|
@ -168,18 +191,24 @@ PyDoc_STRVAR(pysqlite_connection_create_aggregate__doc__,
|
|||
static PyObject *
|
||||
pysqlite_connection_create_aggregate_impl(pysqlite_Connection *self,
|
||||
const char *name, int n_arg,
|
||||
PyObject *aggregate_class);
|
||||
PyObject *aggregate_class,
|
||||
int deterministic, int directonly,
|
||||
int innocuous);
|
||||
|
||||
static PyObject *
|
||||
pysqlite_connection_create_aggregate(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
|
||||
{
|
||||
PyObject *return_value = NULL;
|
||||
static const char * const _keywords[] = {"name", "n_arg", "aggregate_class", NULL};
|
||||
static const char * const _keywords[] = {"name", "n_arg", "aggregate_class", "deterministic", "directonly", "innocuous", NULL};
|
||||
static _PyArg_Parser _parser = {NULL, _keywords, "create_aggregate", 0};
|
||||
PyObject *argsbuf[3];
|
||||
PyObject *argsbuf[6];
|
||||
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 3;
|
||||
const char *name;
|
||||
int n_arg;
|
||||
PyObject *aggregate_class;
|
||||
int deterministic = 0;
|
||||
int directonly = 0;
|
||||
int innocuous = 0;
|
||||
|
||||
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 3, 3, 0, argsbuf);
|
||||
if (!args) {
|
||||
|
@ -203,7 +232,33 @@ pysqlite_connection_create_aggregate(pysqlite_Connection *self, PyObject *const
|
|||
goto exit;
|
||||
}
|
||||
aggregate_class = args[2];
|
||||
return_value = pysqlite_connection_create_aggregate_impl(self, name, n_arg, aggregate_class);
|
||||
if (!noptargs) {
|
||||
goto skip_optional_kwonly;
|
||||
}
|
||||
if (args[3]) {
|
||||
deterministic = PyObject_IsTrue(args[3]);
|
||||
if (deterministic < 0) {
|
||||
goto exit;
|
||||
}
|
||||
if (!--noptargs) {
|
||||
goto skip_optional_kwonly;
|
||||
}
|
||||
}
|
||||
if (args[4]) {
|
||||
directonly = PyObject_IsTrue(args[4]);
|
||||
if (directonly < 0) {
|
||||
goto exit;
|
||||
}
|
||||
if (!--noptargs) {
|
||||
goto skip_optional_kwonly;
|
||||
}
|
||||
}
|
||||
innocuous = PyObject_IsTrue(args[5]);
|
||||
if (innocuous < 0) {
|
||||
goto exit;
|
||||
}
|
||||
skip_optional_kwonly:
|
||||
return_value = pysqlite_connection_create_aggregate_impl(self, name, n_arg, aggregate_class, deterministic, directonly, innocuous);
|
||||
|
||||
exit:
|
||||
return return_value;
|
||||
|
@ -719,4 +774,4 @@ exit:
|
|||
#ifndef PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF
|
||||
#define PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF
|
||||
#endif /* !defined(PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF) */
|
||||
/*[clinic end generated code: output=7cb13d491a5970aa input=a9049054013a1b77]*/
|
||||
/*[clinic end generated code: output=33bcda978b5bc004 input=a9049054013a1b77]*/
|
||||
|
|
|
@ -812,6 +812,64 @@ static void _destructor(void* args)
|
|||
Py_DECREF((PyObject*)args);
|
||||
}
|
||||
|
||||
static int apply_deterministic_flag_if_supported(int *flags, int is_set)
|
||||
{
|
||||
#if SQLITE_VERSION_NUMBER < 3008003
|
||||
PyErr_SetString(pysqlite_NotSupportedError,
|
||||
"deterministic=True requires SQLite 3.8.3 or higher");
|
||||
return -1;
|
||||
#else
|
||||
if (sqlite3_libversion_number() < 3008003) {
|
||||
PyErr_SetString(pysqlite_NotSupportedError,
|
||||
"deterministic=True requires SQLite 3.8.3 or higher");
|
||||
return -1;
|
||||
}
|
||||
if (is_set) {
|
||||
*flags |= SQLITE_DETERMINISTIC;
|
||||
}
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int apply_innocuous_flag_if_supported(int *flags, int is_set)
|
||||
{
|
||||
#if SQLITE_VERSION_NUMBER < 3031000
|
||||
PyErr_SetString(pysqlite_NotSupportedError,
|
||||
"innocuous=True requires SQLite 3.31.0 or higher");
|
||||
return -1;
|
||||
#else
|
||||
if (sqlite3_libversion_number() < 3031000) {
|
||||
PyErr_SetString(pysqlite_NotSupportedError,
|
||||
"innocuous=True requires SQLite 3.31.0 or higher");
|
||||
return -1;
|
||||
}
|
||||
if (is_set) {
|
||||
*flags |= SQLITE_INNOCUOUS;
|
||||
}
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int apply_directonly_flag_if_supported(int *flags, int is_set)
|
||||
{
|
||||
#if SQLITE_VERSION_NUMBER < 3031000
|
||||
PyErr_SetString(pysqlite_NotSupportedError,
|
||||
"directonly=True requires SQLite 3.31.0 or higher");
|
||||
return -1;
|
||||
#else
|
||||
if (sqlite3_libversion_number() < 3031000) {
|
||||
PyErr_SetString(pysqlite_NotSupportedError,
|
||||
"directonly=True requires SQLite 3.31.0 or higher");
|
||||
return -1;
|
||||
}
|
||||
if (is_set) {
|
||||
*flags |= SQLITE_DIRECTONLY;
|
||||
}
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/*[clinic input]
|
||||
_sqlite3.Connection.create_function as pysqlite_connection_create_function
|
||||
|
||||
|
@ -820,6 +878,8 @@ _sqlite3.Connection.create_function as pysqlite_connection_create_function
|
|||
func: object
|
||||
*
|
||||
deterministic: bool = False
|
||||
directonly: bool = False
|
||||
innocuous: bool = False
|
||||
|
||||
Creates a new function. Non-standard.
|
||||
[clinic start generated code]*/
|
||||
|
@ -827,30 +887,27 @@ Creates a new function. Non-standard.
|
|||
static PyObject *
|
||||
pysqlite_connection_create_function_impl(pysqlite_Connection *self,
|
||||
const char *name, int narg,
|
||||
PyObject *func, int deterministic)
|
||||
/*[clinic end generated code: output=07d1877dd98c0308 input=f2edcf073e815beb]*/
|
||||
PyObject *func, int deterministic,
|
||||
int directonly, int innocuous)
|
||||
/*[clinic end generated code: output=985f70400f2127a5 input=6574448150660add]*/
|
||||
{
|
||||
int rc;
|
||||
int flags = SQLITE_UTF8;
|
||||
|
||||
if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (deterministic) {
|
||||
#if SQLITE_VERSION_NUMBER < 3008003
|
||||
PyErr_SetString(pysqlite_NotSupportedError,
|
||||
"deterministic=True requires SQLite 3.8.3 or higher");
|
||||
int flags = SQLITE_UTF8;
|
||||
if (apply_deterministic_flag_if_supported(&flags, deterministic) < 0) {
|
||||
return NULL;
|
||||
#else
|
||||
if (sqlite3_libversion_number() < 3008003) {
|
||||
PyErr_SetString(pysqlite_NotSupportedError,
|
||||
"deterministic=True requires SQLite 3.8.3 or higher");
|
||||
return NULL;
|
||||
}
|
||||
flags |= SQLITE_DETERMINISTIC;
|
||||
#endif
|
||||
}
|
||||
if (apply_directonly_flag_if_supported(&flags, directonly) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (apply_innocuous_flag_if_supported(&flags, innocuous) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rc = sqlite3_create_function_v2(self->db,
|
||||
name,
|
||||
narg,
|
||||
|
@ -875,6 +932,10 @@ _sqlite3.Connection.create_aggregate as pysqlite_connection_create_aggregate
|
|||
name: str
|
||||
n_arg: int
|
||||
aggregate_class: object
|
||||
*
|
||||
deterministic: bool = False
|
||||
directonly: bool = False
|
||||
innocuous: bool = False
|
||||
|
||||
Creates a new aggregate. Non-standard.
|
||||
[clinic start generated code]*/
|
||||
|
@ -882,8 +943,10 @@ Creates a new aggregate. Non-standard.
|
|||
static PyObject *
|
||||
pysqlite_connection_create_aggregate_impl(pysqlite_Connection *self,
|
||||
const char *name, int n_arg,
|
||||
PyObject *aggregate_class)
|
||||
/*[clinic end generated code: output=fbb2f858cfa4d8db input=c2e13bbf234500a5]*/
|
||||
PyObject *aggregate_class,
|
||||
int deterministic, int directonly,
|
||||
int innocuous)
|
||||
/*[clinic end generated code: output=6b1d5a52bc5f8d3e input=8e99cf6ed595b7a3]*/
|
||||
{
|
||||
int rc;
|
||||
|
||||
|
@ -891,10 +954,21 @@ pysqlite_connection_create_aggregate_impl(pysqlite_Connection *self,
|
|||
return NULL;
|
||||
}
|
||||
|
||||
int flags = SQLITE_UTF8;
|
||||
if (apply_deterministic_flag_if_supported(&flags, deterministic) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (apply_directonly_flag_if_supported(&flags, directonly) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (apply_innocuous_flag_if_supported(&flags, innocuous) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rc = sqlite3_create_function_v2(self->db,
|
||||
name,
|
||||
n_arg,
|
||||
SQLITE_UTF8,
|
||||
flags,
|
||||
(void*)Py_NewRef(aggregate_class),
|
||||
0,
|
||||
&_pysqlite_step_callback,
|
||||
|
|
Loading…
Reference in New Issue