From d80daaf38756b494c7f58d13ffa8685e390b3678 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 27 Dec 2020 23:26:47 +0100 Subject: [PATCH] Add support for 'directonly' and 'innocuous' flags for user-defined functions --- Doc/library/sqlite3.rst | 32 ++++- Lib/sqlite3/test/userfunctions.py | 75 ++++++++++++ .../2020-05-25-21-55-24.bpo-40766.bh9Ze5.rst | 2 + Modules/_sqlite/clinic/connection.c.h | 81 ++++++++++--- Modules/_sqlite/connection.c | 110 +++++++++++++++--- 5 files changed, 267 insertions(+), 33 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-05-25-21-55-24.bpo-40766.bh9Ze5.rst diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 950df60b2027de..b61d25f11ae945 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -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 diff --git a/Lib/sqlite3/test/userfunctions.py b/Lib/sqlite3/test/userfunctions.py index c11c82e1275778..da7594f35a6496 100644 --- a/Lib/sqlite3/test/userfunctions.py +++ b/Lib/sqlite3/test/userfunctions.py @@ -316,6 +316,39 @@ def CheckFuncDeterministicKeywordOnly(self): 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 @@ def setUp(self): 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 @@ def CheckAggrCheckAggrSum(self): 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): diff --git a/Misc/NEWS.d/next/Library/2020-05-25-21-55-24.bpo-40766.bh9Ze5.rst b/Misc/NEWS.d/next/Library/2020-05-25-21-55-24.bpo-40766.bh9Ze5.rst new file mode 100644 index 00000000000000..a487423048d92f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-25-21-55-24.bpo-40766.bh9Ze5.rst @@ -0,0 +1,2 @@ +Add support for `SQLITE_INNOCUOUS` and `SQLITE_DIRECTONLY` flags in +:mod:`sqlite3`. diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h index 01b8e37a957fc2..e34fc9f2f69a0f 100644 --- a/Modules/_sqlite/clinic/connection.c.h +++ b/Modules/_sqlite/clinic/connection.c.h @@ -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 @@ pysqlite_connection_exit(pysqlite_Connection *self, PyObject *const *args, Py_ss #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]*/ diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 5c7b4eeae6ae9f..3d0991a70364a3 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -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; + } + if (apply_directonly_flag_if_supported(&flags, directonly) < 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_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,