Skip to content

Commit cde02ae

Browse files
miss-islingtonpicnixzencukou
authored
[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 bc172ee) Co-authored-by: Bénédikt Tran <[email protected]> Co-authored-by: Petr Viktorin <[email protected]>
1 parent cd8fc3a commit cde02ae

File tree

6 files changed

+134
-19
lines changed

6 files changed

+134
-19
lines changed

Doc/library/sqlite3.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1612,6 +1612,9 @@ Cursor objects
16121612
If the *size* parameter is used, then it is best for it to retain the same
16131613
value from one :meth:`fetchmany` call to the next.
16141614

1615+
.. versionchanged:: next
1616+
Negative *size* values are rejected by raising :exc:`ValueError`.
1617+
16151618
.. method:: fetchall()
16161619

16171620
Return all (remaining) rows of a query result as a :class:`list`.
@@ -1639,6 +1642,9 @@ Cursor objects
16391642
Read/write attribute that controls the number of rows returned by :meth:`fetchmany`.
16401643
The default value is 1 which means a single row would be fetched per call.
16411644

1645+
.. versionchanged:: next
1646+
Negative values are rejected by raising :exc:`ValueError`.
1647+
16421648
.. attribute:: connection
16431649

16441650
Read-only attribute that provides the SQLite database :class:`Connection`

Lib/test/test_sqlite3/test_dbapi.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# 3. This notice may not be removed or altered from any source distribution.
2222

2323
import contextlib
24+
import functools
2425
import os
2526
import sqlite3 as sqlite
2627
import subprocess
@@ -1060,7 +1061,7 @@ def test_array_size(self):
10601061
# now set to 2
10611062
self.cu.arraysize = 2
10621063

1063-
# now make the query return 3 rows
1064+
# now make the query return 2 rows from a table of 3 rows
10641065
self.cu.execute("delete from test")
10651066
self.cu.execute("insert into test(name) values ('A')")
10661067
self.cu.execute("insert into test(name) values ('B')")
@@ -1070,13 +1071,50 @@ def test_array_size(self):
10701071

10711072
self.assertEqual(len(res), 2)
10721073

1074+
def test_invalid_array_size(self):
1075+
UINT32_MAX = (1 << 32) - 1
1076+
setter = functools.partial(setattr, self.cu, 'arraysize')
1077+
1078+
self.assertRaises(TypeError, setter, 1.0)
1079+
self.assertRaises(ValueError, setter, -3)
1080+
self.assertRaises(OverflowError, setter, UINT32_MAX + 1)
1081+
10731082
def test_fetchmany(self):
1083+
# no active SQL statement
1084+
res = self.cu.fetchmany()
1085+
self.assertEqual(res, [])
1086+
res = self.cu.fetchmany(1000)
1087+
self.assertEqual(res, [])
1088+
1089+
# test default parameter
1090+
self.cu.execute("select name from test")
1091+
res = self.cu.fetchmany()
1092+
self.assertEqual(len(res), 1)
1093+
1094+
# test when the number of requested rows exceeds the actual count
10741095
self.cu.execute("select name from test")
10751096
res = self.cu.fetchmany(100)
10761097
self.assertEqual(len(res), 1)
10771098
res = self.cu.fetchmany(100)
10781099
self.assertEqual(res, [])
10791100

1101+
# test when size = 0
1102+
self.cu.execute("select name from test")
1103+
res = self.cu.fetchmany(0)
1104+
self.assertEqual(res, [])
1105+
res = self.cu.fetchmany(100)
1106+
self.assertEqual(len(res), 1)
1107+
res = self.cu.fetchmany(100)
1108+
self.assertEqual(res, [])
1109+
1110+
def test_invalid_fetchmany(self):
1111+
UINT32_MAX = (1 << 32) - 1
1112+
fetchmany = self.cu.fetchmany
1113+
1114+
self.assertRaises(TypeError, fetchmany, 1.0)
1115+
self.assertRaises(ValueError, fetchmany, -3)
1116+
self.assertRaises(OverflowError, fetchmany, UINT32_MAX + 1)
1117+
10801118
def test_fetchmany_kw_arg(self):
10811119
"""Checks if fetchmany works with keyword arguments"""
10821120
self.cu.execute("select name from test")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`sqlite3`: correctly handle maximum number of rows to fetch in
2+
:meth:`Cursor.fetchmany <sqlite3.Cursor.fetchmany>` and reject negative
3+
values for :attr:`Cursor.arraysize <sqlite3.Cursor.arraysize>`. Patch by
4+
Bénédikt Tran.

Modules/_sqlite/clinic/cursor.c.h

Lines changed: 47 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_sqlite/cursor.c

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,35 +1162,31 @@ pysqlite_cursor_fetchone_impl(pysqlite_Cursor *self)
11621162
/*[clinic input]
11631163
_sqlite3.Cursor.fetchmany as pysqlite_cursor_fetchmany
11641164
1165-
size as maxrows: int(c_default='((pysqlite_Cursor *)self)->arraysize') = 1
1165+
size as maxrows: uint32(c_default='((pysqlite_Cursor *)self)->arraysize') = 1
11661166
The default value is set by the Cursor.arraysize attribute.
11671167
11681168
Fetches several rows from the resultset.
11691169
[clinic start generated code]*/
11701170

11711171
static PyObject *
1172-
pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, int maxrows)
1173-
/*[clinic end generated code: output=a8ef31fea64d0906 input=035dbe44a1005bf2]*/
1172+
pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, uint32_t maxrows)
1173+
/*[clinic end generated code: output=3325f2b477c71baf input=a509c412aa70b27e]*/
11741174
{
11751175
PyObject* row;
11761176
PyObject* list;
1177-
int counter = 0;
11781177

11791178
list = PyList_New(0);
11801179
if (!list) {
11811180
return NULL;
11821181
}
11831182

1184-
while ((row = pysqlite_cursor_iternext((PyObject *)self))) {
1185-
if (PyList_Append(list, row) < 0) {
1186-
Py_DECREF(row);
1187-
break;
1188-
}
1183+
while (maxrows > 0 && (row = pysqlite_cursor_iternext((PyObject *)self))) {
1184+
int rc = PyList_Append(list, row);
11891185
Py_DECREF(row);
1190-
1191-
if (++counter == maxrows) {
1186+
if (rc < 0) {
11921187
break;
11931188
}
1189+
maxrows--;
11941190
}
11951191

11961192
if (PyErr_Occurred()) {
@@ -1304,6 +1300,30 @@ pysqlite_cursor_close_impl(pysqlite_Cursor *self)
13041300
Py_RETURN_NONE;
13051301
}
13061302

1303+
/*[clinic input]
1304+
@getter
1305+
_sqlite3.Cursor.arraysize
1306+
[clinic start generated code]*/
1307+
1308+
static PyObject *
1309+
_sqlite3_Cursor_arraysize_get_impl(pysqlite_Cursor *self)
1310+
/*[clinic end generated code: output=e0919d97175e6c50 input=3278f8d3ecbd90e3]*/
1311+
{
1312+
return PyLong_FromUInt32(self->arraysize);
1313+
}
1314+
1315+
/*[clinic input]
1316+
@setter
1317+
_sqlite3.Cursor.arraysize
1318+
[clinic start generated code]*/
1319+
1320+
static int
1321+
_sqlite3_Cursor_arraysize_set_impl(pysqlite_Cursor *self, PyObject *value)
1322+
/*[clinic end generated code: output=af59a6b09f8cce6e input=ace48cb114e26060]*/
1323+
{
1324+
return PyLong_AsUInt32(value, &self->arraysize);
1325+
}
1326+
13071327
static PyMethodDef cursor_methods[] = {
13081328
PYSQLITE_CURSOR_CLOSE_METHODDEF
13091329
PYSQLITE_CURSOR_EXECUTEMANY_METHODDEF
@@ -1321,14 +1341,18 @@ static struct PyMemberDef cursor_members[] =
13211341
{
13221342
{"connection", _Py_T_OBJECT, offsetof(pysqlite_Cursor, connection), Py_READONLY},
13231343
{"description", _Py_T_OBJECT, offsetof(pysqlite_Cursor, description), Py_READONLY},
1324-
{"arraysize", Py_T_INT, offsetof(pysqlite_Cursor, arraysize), 0},
13251344
{"lastrowid", _Py_T_OBJECT, offsetof(pysqlite_Cursor, lastrowid), Py_READONLY},
13261345
{"rowcount", Py_T_LONG, offsetof(pysqlite_Cursor, rowcount), Py_READONLY},
13271346
{"row_factory", _Py_T_OBJECT, offsetof(pysqlite_Cursor, row_factory), 0},
13281347
{"__weaklistoffset__", Py_T_PYSSIZET, offsetof(pysqlite_Cursor, in_weakreflist), Py_READONLY},
13291348
{NULL}
13301349
};
13311350

1351+
static struct PyGetSetDef cursor_getsets[] = {
1352+
_SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF
1353+
{NULL},
1354+
};
1355+
13321356
static const char cursor_doc[] =
13331357
PyDoc_STR("SQLite database cursor class.");
13341358

@@ -1339,6 +1363,7 @@ static PyType_Slot cursor_slots[] = {
13391363
{Py_tp_iternext, pysqlite_cursor_iternext},
13401364
{Py_tp_methods, cursor_methods},
13411365
{Py_tp_members, cursor_members},
1366+
{Py_tp_getset, cursor_getsets},
13421367
{Py_tp_init, pysqlite_cursor_init},
13431368
{Py_tp_traverse, cursor_traverse},
13441369
{Py_tp_clear, cursor_clear},

Modules/_sqlite/cursor.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ typedef struct
3535
pysqlite_Connection* connection;
3636
PyObject* description;
3737
PyObject* row_cast_map;
38-
int arraysize;
38+
uint32_t arraysize;
3939
PyObject* lastrowid;
4040
long rowcount;
4141
PyObject* row_factory;

0 commit comments

Comments
 (0)