Skip to content

Commit d80daaf

Browse files
author
Erlend E. Aasland
committed
Add support for 'directonly' and 'innocuous' flags for user-defined functions
1 parent a9621bb commit d80daaf

File tree

5 files changed

+267
-33
lines changed

5 files changed

+267
-33
lines changed

Doc/library/sqlite3.rst

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ Connection Objects
343343
:meth:`~Cursor.executescript` method with the given *sql_script*, and
344344
returns the cursor.
345345

346-
.. method:: create_function(name, num_params, func, *, deterministic=False)
346+
.. method:: create_function(name, num_params, func, *, deterministic=False, directonly=False, innocous=False)
347347

348348
Creates a user-defined function that you can later use from within SQL
349349
statements under the function name *name*. *num_params* is the number of
@@ -355,18 +355,40 @@ Connection Objects
355355
SQLite 3.8.3 or higher, :exc:`NotSupportedError` will be raised if used
356356
with older versions.
357357

358+
The *innocuous* flag means that the function is unlikely to cause problems
359+
even if misused. An innocuous function should have no side effects and
360+
should not depend on any values other than its input parameters.
361+
Developers are advised to avoid using the *innocuous* flag for
362+
application-defined functions unless the function has been carefully
363+
audited and found to be free of potentially security-adverse side-effects
364+
and information-leaks. This flag is supported by SQLite 3.31.0 or higher.
365+
:exc:`NotSupportedError` will be raised if used with older SQLite versions.
366+
367+
The *directonly* flag means that the function may only be invoked from
368+
top-level SQL, and cannot be used in VIEWs or TRIGGERs nor in schema
369+
structures such as CHECK constraints, DEFAULT clauses, expression indexes,
370+
partial indexes, or generated columns. The *directonly* flag is a security
371+
feature which is recommended for all application-defined SQL functions,
372+
and especially for functions that have side-effects or that could
373+
potentially leak sensitive information. This flag is supported by SQLite
374+
3.31.0 or higher. :exc:`NotSupportedError` will be raised if used with
375+
older SQLite versions.
376+
358377
The function can return any of the types supported by SQLite: bytes, str, int,
359378
float and ``None``.
360379

361380
.. versionchanged:: 3.8
362381
The *deterministic* parameter was added.
363382

383+
.. versionchanged:: 3.10
384+
The *innocuous* and *directonly* parameters were added.
385+
364386
Example:
365387

366388
.. literalinclude:: ../includes/sqlite3/md5func.py
367389

368390

369-
.. method:: create_aggregate(name, num_params, aggregate_class)
391+
.. method:: create_aggregate(name, num_params, aggregate_class, directonly=False, innocuous=False)
370392

371393
Creates a user-defined aggregate function.
372394

@@ -378,6 +400,12 @@ Connection Objects
378400
The ``finalize`` method can return any of the types supported by SQLite:
379401
bytes, str, int, float and ``None``.
380402

403+
See :func:`create_function` for a description of the *innocuous* and
404+
*directonly* parameters.
405+
406+
.. versionchanged:: 3.10
407+
The *innocuous* and *directonly* parameters were added.
408+
381409
Example:
382410

383411
.. literalinclude:: ../includes/sqlite3/mysumaggr.py

Lib/sqlite3/test/userfunctions.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,39 @@ def CheckFuncDeterministicKeywordOnly(self):
316316
with self.assertRaises(TypeError):
317317
self.con.create_function("deterministic", 0, int, True)
318318

319+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or higher")
320+
def CheckFuncNonInnocuousInTrustedEnv(self):
321+
mock = unittest.mock.Mock(return_value=None)
322+
self.con.create_function("noninnocuous", 0, mock, innocuous=False)
323+
self.con.execute("pragma trusted_schema = 0")
324+
self.con.execute("drop view if exists notallowed")
325+
self.con.execute("create view notallowed as select noninnocuous() = noninnocuous()")
326+
with self.assertRaises(sqlite.OperationalError) as cm:
327+
self.con.execute("select * from notallowed")
328+
self.assertEqual(str(cm.exception), 'unsafe use of noninnocuous()')
329+
330+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or higher")
331+
def CheckFuncInnocuousInTrustedEnv(self):
332+
mock = unittest.mock.Mock(return_value=None)
333+
self.con.create_function("innocuous", 0, mock, innocuous=True)
334+
self.con.execute("pragma trusted_schema = 0")
335+
self.con.execute("drop view if exists allowed")
336+
self.con.execute("create view allowed as select innocuous() = innocuous()")
337+
self.con.execute("select * from allowed")
338+
self.assertEqual(mock.call_count, 2)
339+
340+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or higher")
341+
def CheckFuncDirectOnly(self):
342+
mock = unittest.mock.Mock(return_value=None)
343+
self.con.create_function("directonly", 0, mock, directonly=True)
344+
self.con.execute("pragma trusted_schema = 1")
345+
self.con.execute("drop view if exists notallowed")
346+
self.con.execute("select directonly() = directonly()")
347+
self.assertEqual(mock.call_count, 2)
348+
self.con.execute("create view notallowed as select directonly() = directonly()")
349+
with self.assertRaises(sqlite.OperationalError) as cm:
350+
self.con.execute("select * from notallowed")
351+
self.assertEqual(str(cm.exception), 'unsafe use of directonly()')
319352

320353
class AggregateTests(unittest.TestCase):
321354
def setUp(self):
@@ -341,6 +374,9 @@ def setUp(self):
341374
self.con.create_aggregate("checkType", 2, AggrCheckType)
342375
self.con.create_aggregate("checkTypes", -1, AggrCheckTypes)
343376
self.con.create_aggregate("mysum", 1, AggrSum)
377+
if sqlite.sqlite_version_info >= (3, 31, 0):
378+
self.con.create_aggregate("mysumInnocuous", 1, AggrSum, innocuous=True)
379+
self.con.create_aggregate("mysumDirectOnly", 1, AggrSum, directonly=True)
344380

345381
def tearDown(self):
346382
#self.cur.close()
@@ -429,6 +465,45 @@ def CheckAggrCheckAggrSum(self):
429465
val = cur.fetchone()[0]
430466
self.assertEqual(val, 60)
431467

468+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or newer")
469+
def CheckAggrNonInnocuous(self):
470+
cur = self.con.cursor()
471+
cur.execute("pragma trusted_schema = 0")
472+
cur.execute("delete from test")
473+
cur.execute("drop view if exists notallowed")
474+
cur.execute("insert into test(i) values (?)", (10,))
475+
cur.execute("create view notallowed as select mysum(i) from test")
476+
with self.assertRaises(sqlite.OperationalError) as cm:
477+
cur.execute("select * from notallowed")
478+
self.assertEqual(str(cm.exception), 'unsafe use of mysum()')
479+
480+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or newer")
481+
def CheckAggrInnocuous(self):
482+
cur = self.con.cursor()
483+
cur.execute("pragma trusted_schema = 0")
484+
cur.execute("delete from test")
485+
cur.execute("drop view if exists allowed")
486+
cur.executemany("insert into test(i) values (?)", [(10,), (20,), (30,)])
487+
cur.execute("create view allowed as select mysumInnocuous(i) from test")
488+
cur.execute("select * from allowed")
489+
val = cur.fetchone()[0]
490+
self.assertEqual(val, 60)
491+
492+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0), "Requires SQLite 3.31.0 or newer")
493+
def CheckAggrDirectOnly(self):
494+
cur = self.con.cursor()
495+
cur.execute("pragma trusted_schema = 1")
496+
cur.execute("delete from test")
497+
cur.execute("drop view if exists notallowed")
498+
cur.executemany("insert into test(i) values (?)", [(10,), (20,), (30,)])
499+
cur.execute("create view notallowed as select mysumDirectOnly(i) from test")
500+
with self.assertRaises(sqlite.OperationalError) as cm:
501+
cur.execute("select * from notallowed")
502+
self.assertEqual(str(cm.exception), 'unsafe use of mysumdirectonly()')
503+
cur.execute("select mysumDirectOnly(i) from test")
504+
val = cur.fetchone()[0]
505+
self.assertEqual(val, 60)
506+
432507
class AuthorizerTests(unittest.TestCase):
433508
@staticmethod
434509
def authorizer_cb(action, arg1, arg2, dbname, source):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add support for `SQLITE_INNOCUOUS` and `SQLITE_DIRECTONLY` flags in
2+
:mod:`sqlite3`.

Modules/_sqlite/clinic/connection.c.h

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ pysqlite_connection_rollback(pysqlite_Connection *self, PyObject *Py_UNUSED(igno
9494
}
9595

9696
PyDoc_STRVAR(pysqlite_connection_create_function__doc__,
97-
"create_function($self, /, name, narg, func, *, deterministic=False)\n"
97+
"create_function($self, /, name, narg, func, *, deterministic=False,\n"
98+
" directonly=False, innocuous=False)\n"
9899
"--\n"
99100
"\n"
100101
"Creates a new function. Non-standard.");
@@ -105,20 +106,23 @@ PyDoc_STRVAR(pysqlite_connection_create_function__doc__,
105106
static PyObject *
106107
pysqlite_connection_create_function_impl(pysqlite_Connection *self,
107108
const char *name, int narg,
108-
PyObject *func, int deterministic);
109+
PyObject *func, int deterministic,
110+
int directonly, int innocuous);
109111

110112
static PyObject *
111113
pysqlite_connection_create_function(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
112114
{
113115
PyObject *return_value = NULL;
114-
static const char * const _keywords[] = {"name", "narg", "func", "deterministic", NULL};
116+
static const char * const _keywords[] = {"name", "narg", "func", "deterministic", "directonly", "innocuous", NULL};
115117
static _PyArg_Parser _parser = {NULL, _keywords, "create_function", 0};
116-
PyObject *argsbuf[4];
118+
PyObject *argsbuf[6];
117119
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 3;
118120
const char *name;
119121
int narg;
120122
PyObject *func;
121123
int deterministic = 0;
124+
int directonly = 0;
125+
int innocuous = 0;
122126

123127
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 3, 3, 0, argsbuf);
124128
if (!args) {
@@ -145,19 +149,38 @@ pysqlite_connection_create_function(pysqlite_Connection *self, PyObject *const *
145149
if (!noptargs) {
146150
goto skip_optional_kwonly;
147151
}
148-
deterministic = PyObject_IsTrue(args[3]);
149-
if (deterministic < 0) {
152+
if (args[3]) {
153+
deterministic = PyObject_IsTrue(args[3]);
154+
if (deterministic < 0) {
155+
goto exit;
156+
}
157+
if (!--noptargs) {
158+
goto skip_optional_kwonly;
159+
}
160+
}
161+
if (args[4]) {
162+
directonly = PyObject_IsTrue(args[4]);
163+
if (directonly < 0) {
164+
goto exit;
165+
}
166+
if (!--noptargs) {
167+
goto skip_optional_kwonly;
168+
}
169+
}
170+
innocuous = PyObject_IsTrue(args[5]);
171+
if (innocuous < 0) {
150172
goto exit;
151173
}
152174
skip_optional_kwonly:
153-
return_value = pysqlite_connection_create_function_impl(self, name, narg, func, deterministic);
175+
return_value = pysqlite_connection_create_function_impl(self, name, narg, func, deterministic, directonly, innocuous);
154176

155177
exit:
156178
return return_value;
157179
}
158180

159181
PyDoc_STRVAR(pysqlite_connection_create_aggregate__doc__,
160-
"create_aggregate($self, /, name, n_arg, aggregate_class)\n"
182+
"create_aggregate($self, /, name, n_arg, aggregate_class, *,\n"
183+
" deterministic=False, directonly=False, innocuous=False)\n"
161184
"--\n"
162185
"\n"
163186
"Creates a new aggregate. Non-standard.");
@@ -168,18 +191,24 @@ PyDoc_STRVAR(pysqlite_connection_create_aggregate__doc__,
168191
static PyObject *
169192
pysqlite_connection_create_aggregate_impl(pysqlite_Connection *self,
170193
const char *name, int n_arg,
171-
PyObject *aggregate_class);
194+
PyObject *aggregate_class,
195+
int deterministic, int directonly,
196+
int innocuous);
172197

173198
static PyObject *
174199
pysqlite_connection_create_aggregate(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
175200
{
176201
PyObject *return_value = NULL;
177-
static const char * const _keywords[] = {"name", "n_arg", "aggregate_class", NULL};
202+
static const char * const _keywords[] = {"name", "n_arg", "aggregate_class", "deterministic", "directonly", "innocuous", NULL};
178203
static _PyArg_Parser _parser = {NULL, _keywords, "create_aggregate", 0};
179-
PyObject *argsbuf[3];
204+
PyObject *argsbuf[6];
205+
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 3;
180206
const char *name;
181207
int n_arg;
182208
PyObject *aggregate_class;
209+
int deterministic = 0;
210+
int directonly = 0;
211+
int innocuous = 0;
183212

184213
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 3, 3, 0, argsbuf);
185214
if (!args) {
@@ -203,7 +232,33 @@ pysqlite_connection_create_aggregate(pysqlite_Connection *self, PyObject *const
203232
goto exit;
204233
}
205234
aggregate_class = args[2];
206-
return_value = pysqlite_connection_create_aggregate_impl(self, name, n_arg, aggregate_class);
235+
if (!noptargs) {
236+
goto skip_optional_kwonly;
237+
}
238+
if (args[3]) {
239+
deterministic = PyObject_IsTrue(args[3]);
240+
if (deterministic < 0) {
241+
goto exit;
242+
}
243+
if (!--noptargs) {
244+
goto skip_optional_kwonly;
245+
}
246+
}
247+
if (args[4]) {
248+
directonly = PyObject_IsTrue(args[4]);
249+
if (directonly < 0) {
250+
goto exit;
251+
}
252+
if (!--noptargs) {
253+
goto skip_optional_kwonly;
254+
}
255+
}
256+
innocuous = PyObject_IsTrue(args[5]);
257+
if (innocuous < 0) {
258+
goto exit;
259+
}
260+
skip_optional_kwonly:
261+
return_value = pysqlite_connection_create_aggregate_impl(self, name, n_arg, aggregate_class, deterministic, directonly, innocuous);
207262

208263
exit:
209264
return return_value;
@@ -719,4 +774,4 @@ pysqlite_connection_exit(pysqlite_Connection *self, PyObject *const *args, Py_ss
719774
#ifndef PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF
720775
#define PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF
721776
#endif /* !defined(PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF) */
722-
/*[clinic end generated code: output=7cb13d491a5970aa input=a9049054013a1b77]*/
777+
/*[clinic end generated code: output=33bcda978b5bc004 input=a9049054013a1b77]*/

0 commit comments

Comments
 (0)