티스토리 수익 글 보기

티스토리 수익 글 보기

[3.2.x] Fixed CVE-2021-23336 — Fixed web cache poisoning via django.… · django/django@be8237c · GitHub
Skip to content

Commit be8237c

Browse files
ngnpopecarltongibson
authored andcommitted
[3.2.x] Fixed CVE-2021-23336 — Fixed web cache poisoning via django.utils.http.parse_qsl().
1 parent 0debc6b commit be8237c

File tree

9 files changed

+94
27
lines changed

9 files changed

+94
27
lines changed

django/http/request.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
# detect whether the max_num_fields argument is available as this security fix
3030
# was backported to Python 3.6.8 and 3.7.2, and may also have been applied by
3131
# downstream package maintainers to other versions in their repositories.
32-
if not func_supports_parameter(parse_qsl, 'max_num_fields'):
32+
if (
33+
not func_supports_parameter(parse_qsl, 'max_num_fields') or
34+
not func_supports_parameter(parse_qsl, 'separator')
35+
):
3336
from django.utils.http import parse_qsl
3437

3538

django/utils/http.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -415,13 +415,13 @@ def _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
415415
# TODO: Remove when dropping support for PY37.
416416
def parse_qsl(
417417
qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8',
418-
errors='replace', max_num_fields=None,
418+
errors='replace', max_num_fields=None, separator='&',
419419
):
420420
"""
421421
Return a list of key/value tuples parsed from query string.
422422
423-
Backport of urllib.parse.parse_qsl() from Python 3.8.
424-
Copyright (C) 2020 Python Software Foundation (see LICENSE.python).
423+
Backport of urllib.parse.parse_qsl() from Python 3.8.8.
424+
Copyright (C) 2021 Python Software Foundation (see LICENSE.python).
425425
426426
----
427427
@@ -447,19 +447,25 @@ def parse_qsl(
447447
max_num_fields: int. If set, then throws a ValueError if there are more
448448
than n fields read by parse_qsl().
449449
450+
separator: str. The symbol to use for separating the query arguments.
451+
Defaults to &.
452+
450453
Returns a list, as G-d intended.
451454
"""
452455
qs, _coerce_result = _coerce_args(qs)
453456

457+
if not separator or not isinstance(separator, (str, bytes)):
458+
raise ValueError('Separator must be of type string or bytes.')
459+
454460
# If max_num_fields is defined then check that the number of fields is less
455461
# than max_num_fields. This prevents a memory exhaustion DOS attack via
456462
# post bodies with many fields.
457463
if max_num_fields is not None:
458-
num_fields = 1 + qs.count('&') + qs.count(';')
464+
num_fields = 1 + qs.count(separator)
459465
if max_num_fields < num_fields:
460466
raise ValueError('Max number of fields exceeded')
461467

462-
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
468+
pairs = [s1 for s1 in qs.split(separator)]
463469
r = []
464470
for name_value in pairs:
465471
if not name_value and not strict_parsing:

docs/releases/2.2.19.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
===========================
2+
Django 2.2.19 release notes
3+
===========================
4+
5+
*February 19, 2021*
6+
7+
Django 2.2.19 fixes a security issue in 2.2.18.
8+
9+
CVE-2021-23336: Web cache poisoning via ``django.utils.http.limited_parse_qsl()``
10+
=================================================================================
11+
12+
Django contains a copy of :func:`urllib.parse.parse_qsl` which was added to
13+
backport some security fixes. A further security fix has been issued recently
14+
such that ``parse_qsl()`` no longer allows using ``;`` as a query parameter
15+
separator by default. Django now includes this fix. See :bpo:`42967` for
16+
further details.

docs/releases/3.0.13.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
===========================
2+
Django 3.0.13 release notes
3+
===========================
4+
5+
*February 19, 2021*
6+
7+
Django 3.0.13 fixes a security issue in 3.0.12.
8+
9+
CVE-2021-23336: Web cache poisoning via ``django.utils.http.limited_parse_qsl()``
10+
=================================================================================
11+
12+
Django contains a copy of :func:`urllib.parse.parse_qsl` which was added to
13+
backport some security fixes. A further security fix has been issued recently
14+
such that ``parse_qsl()`` no longer allows using ``;`` as a query parameter
15+
separator by default. Django now includes this fix. See :bpo:`42967` for
16+
further details.

docs/releases/3.1.7.txt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,18 @@
22
Django 3.1.7 release notes
33
==========================
44

5-
*Expected March 1, 2021*
5+
*February 19, 2021*
66

7-
Django 3.1.7 fixes several bugs in 3.1.6.
7+
Django 3.1.7 fixes a security issue and a bug in 3.1.6.
8+
9+
CVE-2021-23336: Web cache poisoning via ``django.utils.http.limited_parse_qsl()``
10+
=================================================================================
11+
12+
Django contains a copy of :func:`urllib.parse.parse_qsl` which was added to
13+
backport some security fixes. A further security fix has been issued recently
14+
such that ``parse_qsl()`` no longer allows using ``;`` as a query parameter
15+
separator by default. Django now includes this fix. See :bpo:`42967` for
16+
further details.
817

918
Bugfixes
1019
========

docs/releases/index.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ versions of the documentation contain the release notes for any later releases.
4646
.. toctree::
4747
:maxdepth: 1
4848

49+
3.0.13
4950
3.0.12
5051
3.0.11
5152
3.0.10
@@ -65,6 +66,7 @@ versions of the documentation contain the release notes for any later releases.
6566
.. toctree::
6667
:maxdepth: 1
6768

69+
2.2.19
6870
2.2.18
6971
2.2.17
7072
2.2.16

tests/handlers/test_exception.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
class ExceptionHandlerTests(SimpleTestCase):
77

88
def get_suspicious_environ(self):
9-
payload = FakePayload('a=1&a=2;a=3\r\n')
9+
payload = FakePayload('a=1&a=2&a=3\r\n')
1010
return {
1111
'REQUEST_METHOD': 'POST',
1212
'CONTENT_TYPE': 'application/x-www-form-urlencoded',

tests/requests/test_data_upload_settings.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
class DataUploadMaxMemorySizeFormPostTests(SimpleTestCase):
1313
def setUp(self):
14-
payload = FakePayload('a=1&a=2;a=3\r\n')
14+
payload = FakePayload('a=1&a=2&a=3\r\n')
1515
self.request = WSGIRequest({
1616
'REQUEST_METHOD': 'POST',
1717
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
@@ -117,7 +117,7 @@ def test_get_max_fields_exceeded(self):
117117
request = WSGIRequest({
118118
'REQUEST_METHOD': 'GET',
119119
'wsgi.input': BytesIO(b''),
120-
'QUERY_STRING': 'a=1&a=2;a=3',
120+
'QUERY_STRING': 'a=1&a=2&a=3',
121121
})
122122
request.GET['a']
123123

@@ -126,7 +126,7 @@ def test_get_max_fields_not_exceeded(self):
126126
request = WSGIRequest({
127127
'REQUEST_METHOD': 'GET',
128128
'wsgi.input': BytesIO(b''),
129-
'QUERY_STRING': 'a=1&a=2;a=3',
129+
'QUERY_STRING': 'a=1&a=2&a=3',
130130
})
131131
request.GET['a']
132132

@@ -168,7 +168,7 @@ def test_no_limit(self):
168168

169169
class DataUploadMaxNumberOfFieldsFormPost(SimpleTestCase):
170170
def setUp(self):
171-
payload = FakePayload("\r\n".join(['a=1&a=2;a=3', '']))
171+
payload = FakePayload("\r\n".join(['a=1&a=2&a=3', '']))
172172
self.request = WSGIRequest({
173173
'REQUEST_METHOD': 'POST',
174174
'CONTENT_TYPE': 'application/x-www-form-urlencoded',

tests/utils_tests/test_http.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,8 @@ def test(self):
363363

364364

365365
# TODO: Remove when dropping support for PY37. Backport of unit tests for
366-
# urllib.parse.parse_qsl() from Python 3.8. Copyright (C) 2020 Python Software
367-
# Foundation (see LICENSE.python).
366+
# urllib.parse.parse_qsl() from Python 3.8.8. Copyright (C) 2021 Python
367+
# Software Foundation (see LICENSE.python).
368368
class ParseQSLBackportTests(unittest.TestCase):
369369
def test_parse_qsl(self):
370370
tests = [
@@ -388,16 +388,10 @@ def test_parse_qsl(self):
388388
(b'&a=b', [(b'a', b'b')]),
389389
(b'a=a+b&b=b+c', [(b'a', b'a b'), (b'b', b'b c')]),
390390
(b'a=1&a=2', [(b'a', b'1'), (b'a', b'2')]),
391-
(';', []),
392-
(';;', []),
393-
(';a=b', [('a', 'b')]),
394-
('a=a+b;b=b+c', [('a', 'a b'), ('b', 'b c')]),
395-
('a=1;a=2', [('a', '1'), ('a', '2')]),
396-
(b';', []),
397-
(b';;', []),
398-
(b';a=b', [(b'a', b'b')]),
399-
(b'a=a+b;b=b+c', [(b'a', b'a b'), (b'b', b'b c')]),
400-
(b'a=1;a=2', [(b'a', b'1'), (b'a', b'2')]),
391+
(';a=b', [(';a', 'b')]),
392+
('a=a+b;b=b+c', [('a', 'a b;b=b c')]),
393+
(b';a=b', [(b';a', b'b')]),
394+
(b'a=a+b;b=b+c', [(b'a', b'a b;b=b c')]),
401395
]
402396
for original, expected in tests:
403397
with self.subTest(original):
@@ -422,6 +416,27 @@ def test_parse_qsl_encoding(self):
422416
def test_parse_qsl_max_num_fields(self):
423417
with self.assertRaises(ValueError):
424418
parse_qsl('&'.join(['a=a'] * 11), max_num_fields=10)
425-
with self.assertRaises(ValueError):
426-
parse_qsl(';'.join(['a=a'] * 11), max_num_fields=10)
427419
parse_qsl('&'.join(['a=a'] * 10), max_num_fields=10)
420+
421+
def test_parse_qsl_separator(self):
422+
tests = [
423+
(';', []),
424+
(';;', []),
425+
('=;a', []),
426+
(';a=b', [('a', 'b')]),
427+
('a=a+b;b=b+c', [('a', 'a b'), ('b', 'b c')]),
428+
('a=1;a=2', [('a', '1'), ('a', '2')]),
429+
(b';', []),
430+
(b';;', []),
431+
(b';a=b', [(b'a', b'b')]),
432+
(b'a=a+b;b=b+c', [(b'a', b'a b'), (b'b', b'b c')]),
433+
(b'a=1;a=2', [(b'a', b'1'), (b'a', b'2')]),
434+
]
435+
for original, expected in tests:
436+
with self.subTest(original):
437+
result = parse_qsl(original, separator=';')
438+
self.assertEqual(result, expected, 'Error parsing %r' % original)
439+
440+
def test_parse_qsl_bad_separator(self):
441+
with self.assertRaisesRegex(ValueError, 'Separator must be of type string or bytes.'):
442+
parse_qsl('a=b0c=d', separator=0)

0 commit comments

Comments
 (0)