https://github.com/python/cpython/commit/bc172ee8307431caf4c89612e9e454081635191f
commit: bc172ee8307431caf4c89612e9e454081635191f
branch: main
author: Bénédikt Tran <[email protected]>
committer: picnixz <[email protected]>
date: 2025-09-30T11:18:55+02:00
summary:
gh-139283: correctly handle `size` limit in `cursor.fetchmany()` (#139296)
Passing a negative or zero size to `cursor.fetchmany()` made it fetch all rows
instead of none.
While this could be considered a security vulnerability, it was decided to treat
this issue as a regular bug as passing a non-sanitized *size* value in the first
place is not recommended.
files:
A Misc/NEWS.d/next/Security/2025-09-24-13-39-56.gh-issue-139283.jODz_q.rst
M Doc/library/sqlite3.rst
M Lib/test/test_sqlite3/test_dbapi.py
M Modules/_sqlite/clinic/cursor.c.h
M Modules/_sqlite/cursor.c
M Modules/_sqlite/cursor.h
diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst
index 6a7e15db223210..a7c9923f116f9f 100644
--- a/Doc/library/sqlite3.rst
+++ b/Doc/library/sqlite3.rst
@@ -1611,6 +1611,9 @@ Cursor objects
If the *size* parameter is used, then it is best for it to retain the
same
value from one :meth:`fetchmany` call to the next.
+ .. versionchanged:: next
+ Negative *size* values are rejected by raising :exc:`ValueError`.
+
.. method:: fetchall()
Return all (remaining) rows of a query result as a :class:`list`.
@@ -1638,6 +1641,9 @@ Cursor objects
Read/write attribute that controls the number of rows returned by
:meth:`fetchmany`.
The default value is 1 which means a single row would be fetched per
call.
+ .. versionchanged:: next
+ Negative values are rejected by raising :exc:`ValueError`.
+
.. attribute:: connection
Read-only attribute that provides the SQLite database :class:`Connection`
diff --git a/Lib/test/test_sqlite3/test_dbapi.py
b/Lib/test/test_sqlite3/test_dbapi.py
index 74a511ba7c88c2..20e39f61e4dedb 100644
--- a/Lib/test/test_sqlite3/test_dbapi.py
+++ b/Lib/test/test_sqlite3/test_dbapi.py
@@ -21,6 +21,7 @@
# 3. This notice may not be removed or altered from any source distribution.
import contextlib
+import functools
import os
import sqlite3 as sqlite
import subprocess
@@ -1060,7 +1061,7 @@ def test_array_size(self):
# now set to 2
self.cu.arraysize = 2
- # now make the query return 3 rows
+ # now make the query return 2 rows from a table of 3 rows
self.cu.execute("delete from test")
self.cu.execute("insert into test(name) values ('A')")
self.cu.execute("insert into test(name) values ('B')")
@@ -1070,13 +1071,50 @@ def test_array_size(self):
self.assertEqual(len(res), 2)
+ def test_invalid_array_size(self):
+ UINT32_MAX = (1 << 32) - 1
+ setter = functools.partial(setattr, self.cu, 'arraysize')
+
+ self.assertRaises(TypeError, setter, 1.0)
+ self.assertRaises(ValueError, setter, -3)
+ self.assertRaises(OverflowError, setter, UINT32_MAX + 1)
+
def test_fetchmany(self):
+ # no active SQL statement
+ res = self.cu.fetchmany()
+ self.assertEqual(res, [])
+ res = self.cu.fetchmany(1000)
+ self.assertEqual(res, [])
+
+ # test default parameter
+ self.cu.execute("select name from test")
+ res = self.cu.fetchmany()
+ self.assertEqual(len(res), 1)
+
+ # test when the number of requested rows exceeds the actual count
self.cu.execute("select name from test")
res = self.cu.fetchmany(100)
self.assertEqual(len(res), 1)
res = self.cu.fetchmany(100)
self.assertEqual(res, [])
+ # test when size = 0
+ self.cu.execute("select name from test")
+ res = self.cu.fetchmany(0)
+ self.assertEqual(res, [])
+ res = self.cu.fetchmany(100)
+ self.assertEqual(len(res), 1)
+ res = self.cu.fetchmany(100)
+ self.assertEqual(res, [])
+
+ def test_invalid_fetchmany(self):
+ UINT32_MAX = (1 << 32) - 1
+ fetchmany = self.cu.fetchmany
+
+ self.assertRaises(TypeError, fetchmany, 1.0)
+ self.assertRaises(ValueError, fetchmany, -3)
+ self.assertRaises(OverflowError, fetchmany, UINT32_MAX + 1)
+
def test_fetchmany_kw_arg(self):
"""Checks if fetchmany works with keyword arguments"""
self.cu.execute("select name from test")
diff --git
a/Misc/NEWS.d/next/Security/2025-09-24-13-39-56.gh-issue-139283.jODz_q.rst
b/Misc/NEWS.d/next/Security/2025-09-24-13-39-56.gh-issue-139283.jODz_q.rst
new file mode 100644
index 00000000000000..a8fd83bca52554
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2025-09-24-13-39-56.gh-issue-139283.jODz_q.rst
@@ -0,0 +1,4 @@
+:mod:`sqlite3`: correctly handle maximum number of rows to fetch in
+:meth:`Cursor.fetchmany <sqlite3.Cursor.fetchmany>` and reject negative
+values for :attr:`Cursor.arraysize <sqlite3.Cursor.arraysize>`. Patch by
+Bénédikt Tran.
diff --git a/Modules/_sqlite/clinic/cursor.c.h
b/Modules/_sqlite/clinic/cursor.c.h
index 350577f488df4b..3cad9f3aef5ecd 100644
--- a/Modules/_sqlite/clinic/cursor.c.h
+++ b/Modules/_sqlite/clinic/cursor.c.h
@@ -6,6 +6,7 @@ preserve
# include "pycore_gc.h" // PyGC_Head
# include "pycore_runtime.h" // _Py_ID()
#endif
+#include "pycore_long.h" // _PyLong_UInt32_Converter()
#include "pycore_modsupport.h" // _PyArg_CheckPositional()
static int
@@ -181,7 +182,7 @@ PyDoc_STRVAR(pysqlite_cursor_fetchmany__doc__,
{"fetchmany", _PyCFunction_CAST(pysqlite_cursor_fetchmany),
METH_FASTCALL|METH_KEYWORDS, pysqlite_cursor_fetchmany__doc__},
static PyObject *
-pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, int maxrows);
+pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, uint32_t maxrows);
static PyObject *
pysqlite_cursor_fetchmany(PyObject *self, PyObject *const *args, Py_ssize_t
nargs, PyObject *kwnames)
@@ -216,7 +217,7 @@ pysqlite_cursor_fetchmany(PyObject *self, PyObject *const
*args, Py_ssize_t narg
#undef KWTUPLE
PyObject *argsbuf[1];
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) -
0;
- int maxrows = ((pysqlite_Cursor *)self)->arraysize;
+ uint32_t maxrows = ((pysqlite_Cursor *)self)->arraysize;
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
/*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
@@ -226,8 +227,7 @@ pysqlite_cursor_fetchmany(PyObject *self, PyObject *const
*args, Py_ssize_t narg
if (!noptargs) {
goto skip_optional_pos;
}
- maxrows = PyLong_AsInt(args[0]);
- if (maxrows == -1 && PyErr_Occurred()) {
+ if (!_PyLong_UInt32_Converter(args[0], &maxrows)) {
goto exit;
}
skip_optional_pos:
@@ -329,4 +329,46 @@ pysqlite_cursor_close(PyObject *self, PyObject
*Py_UNUSED(ignored))
{
return pysqlite_cursor_close_impl((pysqlite_Cursor *)self);
}
-/*[clinic end generated code: output=d05c7cbbc8bcab26 input=a9049054013a1b77]*/
+
+#if !defined(_sqlite3_Cursor_arraysize_DOCSTR)
+# define _sqlite3_Cursor_arraysize_DOCSTR NULL
+#endif
+#if defined(_SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF)
+# undef _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF
+# define _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF {"arraysize",
(getter)_sqlite3_Cursor_arraysize_get, (setter)_sqlite3_Cursor_arraysize_set,
_sqlite3_Cursor_arraysize_DOCSTR},
+#else
+# define _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF {"arraysize",
(getter)_sqlite3_Cursor_arraysize_get, NULL, _sqlite3_Cursor_arraysize_DOCSTR},
+#endif
+
+static PyObject *
+_sqlite3_Cursor_arraysize_get_impl(pysqlite_Cursor *self);
+
+static PyObject *
+_sqlite3_Cursor_arraysize_get(PyObject *self, void *Py_UNUSED(context))
+{
+ return _sqlite3_Cursor_arraysize_get_impl((pysqlite_Cursor *)self);
+}
+
+#if !defined(_sqlite3_Cursor_arraysize_DOCSTR)
+# define _sqlite3_Cursor_arraysize_DOCSTR NULL
+#endif
+#if defined(_SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF)
+# undef _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF
+# define _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF {"arraysize",
(getter)_sqlite3_Cursor_arraysize_get, (setter)_sqlite3_Cursor_arraysize_set,
_sqlite3_Cursor_arraysize_DOCSTR},
+#else
+# define _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF {"arraysize", NULL,
(setter)_sqlite3_Cursor_arraysize_set, NULL},
+#endif
+
+static int
+_sqlite3_Cursor_arraysize_set_impl(pysqlite_Cursor *self, PyObject *value);
+
+static int
+_sqlite3_Cursor_arraysize_set(PyObject *self, PyObject *value, void
*Py_UNUSED(context))
+{
+ int return_value;
+
+ return_value = _sqlite3_Cursor_arraysize_set_impl((pysqlite_Cursor *)self,
value);
+
+ return return_value;
+}
+/*[clinic end generated code: output=a0e3ebba9e4d0ece input=a9049054013a1b77]*/
diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c
index 0c3f43d0e50b43..2bca411bfd9ba2 100644
--- a/Modules/_sqlite/cursor.c
+++ b/Modules/_sqlite/cursor.c
@@ -1159,35 +1159,31 @@ pysqlite_cursor_fetchone_impl(pysqlite_Cursor *self)
/*[clinic input]
_sqlite3.Cursor.fetchmany as pysqlite_cursor_fetchmany
- size as maxrows: int(c_default='((pysqlite_Cursor *)self)->arraysize') = 1
+ size as maxrows: uint32(c_default='((pysqlite_Cursor *)self)->arraysize')
= 1
The default value is set by the Cursor.arraysize attribute.
Fetches several rows from the resultset.
[clinic start generated code]*/
static PyObject *
-pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, int maxrows)
-/*[clinic end generated code: output=a8ef31fea64d0906 input=035dbe44a1005bf2]*/
+pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, uint32_t maxrows)
+/*[clinic end generated code: output=3325f2b477c71baf input=a509c412aa70b27e]*/
{
PyObject* row;
PyObject* list;
- int counter = 0;
list = PyList_New(0);
if (!list) {
return NULL;
}
- while ((row = pysqlite_cursor_iternext((PyObject *)self))) {
- if (PyList_Append(list, row) < 0) {
- Py_DECREF(row);
- break;
- }
+ while (maxrows > 0 && (row = pysqlite_cursor_iternext((PyObject *)self))) {
+ int rc = PyList_Append(list, row);
Py_DECREF(row);
-
- if (++counter == maxrows) {
+ if (rc < 0) {
break;
}
+ maxrows--;
}
if (PyErr_Occurred()) {
@@ -1301,6 +1297,30 @@ pysqlite_cursor_close_impl(pysqlite_Cursor *self)
Py_RETURN_NONE;
}
+/*[clinic input]
+@getter
+_sqlite3.Cursor.arraysize
+[clinic start generated code]*/
+
+static PyObject *
+_sqlite3_Cursor_arraysize_get_impl(pysqlite_Cursor *self)
+/*[clinic end generated code: output=e0919d97175e6c50 input=3278f8d3ecbd90e3]*/
+{
+ return PyLong_FromUInt32(self->arraysize);
+}
+
+/*[clinic input]
+@setter
+_sqlite3.Cursor.arraysize
+[clinic start generated code]*/
+
+static int
+_sqlite3_Cursor_arraysize_set_impl(pysqlite_Cursor *self, PyObject *value)
+/*[clinic end generated code: output=af59a6b09f8cce6e input=ace48cb114e26060]*/
+{
+ return PyLong_AsUInt32(value, &self->arraysize);
+}
+
static PyMethodDef cursor_methods[] = {
PYSQLITE_CURSOR_CLOSE_METHODDEF
PYSQLITE_CURSOR_EXECUTEMANY_METHODDEF
@@ -1318,7 +1338,6 @@ static struct PyMemberDef cursor_members[] =
{
{"connection", _Py_T_OBJECT, offsetof(pysqlite_Cursor, connection),
Py_READONLY},
{"description", _Py_T_OBJECT, offsetof(pysqlite_Cursor, description),
Py_READONLY},
- {"arraysize", Py_T_INT, offsetof(pysqlite_Cursor, arraysize), 0},
{"lastrowid", _Py_T_OBJECT, offsetof(pysqlite_Cursor, lastrowid),
Py_READONLY},
{"rowcount", Py_T_LONG, offsetof(pysqlite_Cursor, rowcount), Py_READONLY},
{"row_factory", _Py_T_OBJECT, offsetof(pysqlite_Cursor, row_factory), 0},
@@ -1326,6 +1345,11 @@ static struct PyMemberDef cursor_members[] =
{NULL}
};
+static struct PyGetSetDef cursor_getsets[] = {
+ _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF
+ {NULL},
+};
+
static const char cursor_doc[] =
PyDoc_STR("SQLite database cursor class.");
@@ -1336,6 +1360,7 @@ static PyType_Slot cursor_slots[] = {
{Py_tp_iternext, pysqlite_cursor_iternext},
{Py_tp_methods, cursor_methods},
{Py_tp_members, cursor_members},
+ {Py_tp_getset, cursor_getsets},
{Py_tp_init, pysqlite_cursor_init},
{Py_tp_traverse, cursor_traverse},
{Py_tp_clear, cursor_clear},
diff --git a/Modules/_sqlite/cursor.h b/Modules/_sqlite/cursor.h
index 42f817af7c54ad..c840a3d7ed0d15 100644
--- a/Modules/_sqlite/cursor.h
+++ b/Modules/_sqlite/cursor.h
@@ -35,7 +35,7 @@ typedef struct
pysqlite_Connection* connection;
PyObject* description;
PyObject* row_cast_map;
- int arraysize;
+ uint32_t arraysize;
PyObject* lastrowid;
long rowcount;
PyObject* row_factory;
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]