[3.14] gh-139283: correctly handle size limit in cursor.fetchmany() (GH-139296) (GH-139441)

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.
(cherry picked from commit bc172ee830)

Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
This commit is contained in:
Miss Islington (bot) 2025-10-07 21:23:08 +02:00 committed by GitHub
parent cd8fc3aad3
commit cde02ae782
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 134 additions and 19 deletions

View file

@ -1612,6 +1612,9 @@ Cursor objects
If the *size* parameter is used, then it is best for it to retain the same 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. value from one :meth:`fetchmany` call to the next.
.. versionchanged:: next
Negative *size* values are rejected by raising :exc:`ValueError`.
.. method:: fetchall() .. method:: fetchall()
Return all (remaining) rows of a query result as a :class:`list`. Return all (remaining) rows of a query result as a :class:`list`.
@ -1639,6 +1642,9 @@ Cursor objects
Read/write attribute that controls the number of rows returned by :meth:`fetchmany`. 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. 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 .. attribute:: connection
Read-only attribute that provides the SQLite database :class:`Connection` Read-only attribute that provides the SQLite database :class:`Connection`

View file

@ -21,6 +21,7 @@
# 3. This notice may not be removed or altered from any source distribution. # 3. This notice may not be removed or altered from any source distribution.
import contextlib import contextlib
import functools
import os import os
import sqlite3 as sqlite import sqlite3 as sqlite
import subprocess import subprocess
@ -1060,7 +1061,7 @@ def test_array_size(self):
# now set to 2 # now set to 2
self.cu.arraysize = 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("delete from test")
self.cu.execute("insert into test(name) values ('A')") self.cu.execute("insert into test(name) values ('A')")
self.cu.execute("insert into test(name) values ('B')") self.cu.execute("insert into test(name) values ('B')")
@ -1070,13 +1071,50 @@ def test_array_size(self):
self.assertEqual(len(res), 2) 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): 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") self.cu.execute("select name from test")
res = self.cu.fetchmany(100) res = self.cu.fetchmany(100)
self.assertEqual(len(res), 1) self.assertEqual(len(res), 1)
res = self.cu.fetchmany(100) res = self.cu.fetchmany(100)
self.assertEqual(res, []) 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): def test_fetchmany_kw_arg(self):
"""Checks if fetchmany works with keyword arguments""" """Checks if fetchmany works with keyword arguments"""
self.cu.execute("select name from test") self.cu.execute("select name from test")

View file

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

View file

@ -6,6 +6,7 @@ preserve
# include "pycore_gc.h" // PyGC_Head # include "pycore_gc.h" // PyGC_Head
# include "pycore_runtime.h" // _Py_ID() # include "pycore_runtime.h" // _Py_ID()
#endif #endif
#include "pycore_long.h" // _PyLong_UInt32_Converter()
#include "pycore_modsupport.h" // _PyArg_CheckPositional() #include "pycore_modsupport.h" // _PyArg_CheckPositional()
static int 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__}, {"fetchmany", _PyCFunction_CAST(pysqlite_cursor_fetchmany), METH_FASTCALL|METH_KEYWORDS, pysqlite_cursor_fetchmany__doc__},
static PyObject * static PyObject *
pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, int maxrows); pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, uint32_t maxrows);
static PyObject * static PyObject *
pysqlite_cursor_fetchmany(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) 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 #undef KWTUPLE
PyObject *argsbuf[1]; PyObject *argsbuf[1];
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; 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, args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
/*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); /*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) { if (!noptargs) {
goto skip_optional_pos; goto skip_optional_pos;
} }
maxrows = PyLong_AsInt(args[0]); if (!_PyLong_UInt32_Converter(args[0], &maxrows)) {
if (maxrows == -1 && PyErr_Occurred()) {
goto exit; goto exit;
} }
skip_optional_pos: skip_optional_pos:
@ -329,4 +329,46 @@ pysqlite_cursor_close(PyObject *self, PyObject *Py_UNUSED(ignored))
{ {
return pysqlite_cursor_close_impl((pysqlite_Cursor *)self); 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]*/

View file

@ -1162,35 +1162,31 @@ pysqlite_cursor_fetchone_impl(pysqlite_Cursor *self)
/*[clinic input] /*[clinic input]
_sqlite3.Cursor.fetchmany as pysqlite_cursor_fetchmany _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. The default value is set by the Cursor.arraysize attribute.
Fetches several rows from the resultset. Fetches several rows from the resultset.
[clinic start generated code]*/ [clinic start generated code]*/
static PyObject * static PyObject *
pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, int maxrows) pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, uint32_t maxrows)
/*[clinic end generated code: output=a8ef31fea64d0906 input=035dbe44a1005bf2]*/ /*[clinic end generated code: output=3325f2b477c71baf input=a509c412aa70b27e]*/
{ {
PyObject* row; PyObject* row;
PyObject* list; PyObject* list;
int counter = 0;
list = PyList_New(0); list = PyList_New(0);
if (!list) { if (!list) {
return NULL; return NULL;
} }
while ((row = pysqlite_cursor_iternext((PyObject *)self))) { while (maxrows > 0 && (row = pysqlite_cursor_iternext((PyObject *)self))) {
if (PyList_Append(list, row) < 0) { int rc = PyList_Append(list, row);
Py_DECREF(row);
break;
}
Py_DECREF(row); Py_DECREF(row);
if (rc < 0) {
if (++counter == maxrows) {
break; break;
} }
maxrows--;
} }
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
@ -1304,6 +1300,30 @@ pysqlite_cursor_close_impl(pysqlite_Cursor *self)
Py_RETURN_NONE; 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[] = { static PyMethodDef cursor_methods[] = {
PYSQLITE_CURSOR_CLOSE_METHODDEF PYSQLITE_CURSOR_CLOSE_METHODDEF
PYSQLITE_CURSOR_EXECUTEMANY_METHODDEF PYSQLITE_CURSOR_EXECUTEMANY_METHODDEF
@ -1321,7 +1341,6 @@ static struct PyMemberDef cursor_members[] =
{ {
{"connection", _Py_T_OBJECT, offsetof(pysqlite_Cursor, connection), Py_READONLY}, {"connection", _Py_T_OBJECT, offsetof(pysqlite_Cursor, connection), Py_READONLY},
{"description", _Py_T_OBJECT, offsetof(pysqlite_Cursor, description), 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}, {"lastrowid", _Py_T_OBJECT, offsetof(pysqlite_Cursor, lastrowid), Py_READONLY},
{"rowcount", Py_T_LONG, offsetof(pysqlite_Cursor, rowcount), Py_READONLY}, {"rowcount", Py_T_LONG, offsetof(pysqlite_Cursor, rowcount), Py_READONLY},
{"row_factory", _Py_T_OBJECT, offsetof(pysqlite_Cursor, row_factory), 0}, {"row_factory", _Py_T_OBJECT, offsetof(pysqlite_Cursor, row_factory), 0},
@ -1329,6 +1348,11 @@ static struct PyMemberDef cursor_members[] =
{NULL} {NULL}
}; };
static struct PyGetSetDef cursor_getsets[] = {
_SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF
{NULL},
};
static const char cursor_doc[] = static const char cursor_doc[] =
PyDoc_STR("SQLite database cursor class."); PyDoc_STR("SQLite database cursor class.");
@ -1339,6 +1363,7 @@ static PyType_Slot cursor_slots[] = {
{Py_tp_iternext, pysqlite_cursor_iternext}, {Py_tp_iternext, pysqlite_cursor_iternext},
{Py_tp_methods, cursor_methods}, {Py_tp_methods, cursor_methods},
{Py_tp_members, cursor_members}, {Py_tp_members, cursor_members},
{Py_tp_getset, cursor_getsets},
{Py_tp_init, pysqlite_cursor_init}, {Py_tp_init, pysqlite_cursor_init},
{Py_tp_traverse, cursor_traverse}, {Py_tp_traverse, cursor_traverse},
{Py_tp_clear, cursor_clear}, {Py_tp_clear, cursor_clear},

View file

@ -35,7 +35,7 @@ typedef struct
pysqlite_Connection* connection; pysqlite_Connection* connection;
PyObject* description; PyObject* description;
PyObject* row_cast_map; PyObject* row_cast_map;
int arraysize; uint32_t arraysize;
PyObject* lastrowid; PyObject* lastrowid;
long rowcount; long rowcount;
PyObject* row_factory; PyObject* row_factory;