Skip to content

Commit d3b2133

Browse files
committed
change: Enforce part_limit for url-encoded data in parse_form_data().
The behavior is now consistent with the multipart handling and adds an important safeguard against denial of service attacks.
1 parent 74eade5 commit d3b2133

3 files changed

Lines changed: 44 additions & 14 deletions

File tree

CHANGELOG.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ releases.
1414
Release 1.4 (not released yet)
1515
==============================
1616

17+
* feat: Hardened (and faster) header validation.
1718
* change: Dropped support for Python 3.8 and 3.8 (both EOL)
18-
* change: Raise mor helpful :exc:`ParserStateError` instead of implicit
19+
* change: Raise more helpful :exc:`ParserStateError` instead of implicit
1920
:exc:`AssertionError` or :exc:`TypeError` when the parser is used
2021
incorrectly.
21-
* feat: Hardened (and faster) header validation.
22-
* git: Change default brach to `main`
22+
* change: Enforce `part_limit` (128 by default) for url-encoded data in
23+
`parse_form_data()`. This is consistend with the handling of multipart
24+
and an improtant safeguard against denial of service.
25+
* change: New strict-mode check to reject extremely large boundaries.
26+
* build: Change default brach to `main`.
2327

2428

2529
Release 1.3
@@ -99,6 +103,7 @@ parser, which now serves as the basis for all other parsers.
99103
* change: Default disk and memory limits for ``MultipartParser`` increased, but multiple other limits were introduced to allow finer control. Check if the new defaults still fit your needs.
100104
* change: Several undocumented APIs were deprecated or removed, some of which were not strictly private but should only be used by the parser itself, not by applications.
101105

106+
102107
Release 0.2
103108
===========
104109

multipart.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
Awaitable
4444
)
4545

46-
from urllib.parse import parse_qs
46+
from urllib.parse import unquote_plus as _unquote_plus
4747
from wsgiref.headers import Headers
4848
from collections.abc import MutableMapping as DictMixin
4949
import tempfile
@@ -1154,8 +1154,11 @@ def parse_form_data(
11541154
instances). Text fields that are too big to fit into memory limits are
11551155
treated as file uploads with no filename.
11561156
1157-
In case of an url-encoded form request, the total request body size is
1158-
limited by `memory_limit`. Larger requests will trigger an error.
1157+
The default limits for :class:`MultipartParser` apply, but can be
1158+
overridden via keyword arguments. For url-encoded requests, only
1159+
`memory_limit` and `part_limit` have an effect. They have the same
1160+
defaults and meaning as with :class:`MultipartParser` and limit the
1161+
total size and the maximum number of form fields to parse.
11591162
11601163
:param environ: A WSGI environment dictionary. Only `wsgi.input`,
11611164
`CONTENT_TYPE` and `CONTENT_LENGTH` are used.
@@ -1165,9 +1168,8 @@ def parse_form_data(
11651168
results may be empty or incomplete. If False, then exceptions are
11661169
not suppressed. A value of None (default) throws exceptions in
11671170
strict mode but suppresses errors in non-strict mode.
1168-
:param kwargs: Additional keyword arguments are forwarded to
1169-
:class:`MultipartParser`. This is particularly useful to change the
1170-
default parser limits.
1171+
:param kwargs: Additional keyword arguments (e.g. limits) passed to
1172+
the :class:`MultipartParser`.
11711173
:raises MultipartError: See `ignore_errors` parameters.
11721174
"""
11731175

@@ -1217,6 +1219,8 @@ def parse_form_data(
12171219
mem_limit = kwargs.get(
12181220
"memory_limit", kwargs.get("mem_limit", 1024 * 64 * 128)
12191221
)
1222+
part_limit = kwargs.get("part_limit", 128)
1223+
12201224
if content_length > -1:
12211225
if content_length > mem_limit:
12221226
raise ParserLimitReached("Memory limit exceeded")
@@ -1228,12 +1232,18 @@ def parse_form_data(
12281232
if len(data) > mem_limit:
12291233
raise ParserLimitReached("Memory limit exceeded")
12301234

1231-
data = data.decode(charset)
1232-
data = parse_qs(data, keep_blank_values=True, encoding=charset)
1235+
fields = data.decode(charset).split("&", part_limit)
1236+
if len(fields) > part_limit:
1237+
raise ParserLimitReached("Part limit exceeded")
1238+
for field in fields:
1239+
name, sep, value = field.partition("=")
1240+
if not name:
1241+
continue
1242+
forms.append(
1243+
_unquote_plus(name, encoding=charset, errors="replace"),
1244+
_unquote_plus(value or "", encoding=charset, errors="replace"),
1245+
)
12331246

1234-
for key, values in data.items():
1235-
for value in values:
1236-
forms.append(key, value)
12371247
elif strict:
12381248
raise StrictParserError("Unsupported Content-Type")
12391249

test/test_wsgi_parser.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,21 @@ def test_content_length(self):
134134
self.environ['CONTENT_LENGTH'] = '10'
135135
self.parse_form_data(strict=True)
136136

137+
def test_urlencoded_part_limit_exact(self):
138+
self.reset().write('a=1&b=2&c=3')
139+
self.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
140+
forms, files = self.parse_form_data(part_limit=3, strict=True)
141+
self.assertEqual(forms['a'], '1')
142+
self.assertEqual(forms['b'], '2')
143+
self.assertEqual(forms['c'], '3')
144+
self.assertEqual(len(files), 0)
145+
146+
def test_urlencoded_part_limit_exceeded(self):
147+
self.reset().write('a=1&b=2&c=3')
148+
self.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
149+
with self.assertRaises(multipart.ParserLimitReached):
150+
self.parse_form_data(part_limit=2, strict=True)
151+
137152
def test_close_on_error(self):
138153
self.write_field("file1", 'x'*1024, filename="foo.bin")
139154
self.write_field("file2", 'x'*1025, filename="foo.bin")

0 commit comments

Comments
 (0)