Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 326a926

Browse files
ngnpopecarltongibson
authored andcommitted
[3.0.x] Fixed CVE-2021-23336 -- Fixed web cache poisoning via django.utils.http.limited_parse_qsl().
1 parent ad36388 commit 326a926

File tree

7 files changed

+89
-9
lines changed

7 files changed

+89
-9
lines changed

django/utils/http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
RFC3986_GENDELIMS = ":/?#[]@"
4242
RFC3986_SUBDELIMS = "!$&'()*+,;="
4343

44-
FIELDS_MATCH = re.compile('[&;]')
44+
FIELDS_MATCH = re.compile('&')
4545

4646

4747
@keep_lazy_text

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/index.txt

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

28+
3.0.13
2829
3.0.12
2930
3.0.11
3031
3.0.10
@@ -44,6 +45,7 @@ versions of the documentation contain the release notes for any later releases.
4445
.. toctree::
4546
:maxdepth: 1
4647

48+
2.2.19
4749
2.2.18
4850
2.2.17
4951
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: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
from datetime import datetime
44
from unittest import mock
55

6+
from django.core.exceptions import TooManyFieldsSent
67
from django.test import SimpleTestCase, ignore_warnings
78
from django.utils.datastructures import MultiValueDict
89
from django.utils.deprecation import RemovedInDjango40Warning
910
from django.utils.http import (
1011
base36_to_int, escape_leading_slashes, http_date, int_to_base36,
11-
is_safe_url, is_same_domain, parse_etags, parse_http_date, quote_etag,
12-
url_has_allowed_host_and_scheme, urlencode, urlquote, urlquote_plus,
13-
urlsafe_base64_decode, urlsafe_base64_encode, urlunquote, urlunquote_plus,
12+
is_safe_url, is_same_domain, limited_parse_qsl, parse_etags,
13+
parse_http_date, quote_etag, url_has_allowed_host_and_scheme, urlencode,
14+
urlquote, urlquote_plus, urlsafe_base64_decode, urlsafe_base64_encode,
15+
urlunquote, urlunquote_plus,
1416
)
1517

1618

@@ -359,3 +361,47 @@ def test(self):
359361
for url, expected in tests:
360362
with self.subTest(url=url):
361363
self.assertEqual(escape_leading_slashes(url), expected)
364+
365+
366+
# Backport of unit tests for urllib.parse.parse_qsl() from Python 3.8.8.
367+
# Copyright (C) 2021 Python Software Foundation (see LICENSE.python).
368+
class ParseQSLBackportTests(unittest.TestCase):
369+
def test_parse_qsl(self):
370+
tests = [
371+
('', []),
372+
('&', []),
373+
('&&', []),
374+
('=', [('', '')]),
375+
('=a', [('', 'a')]),
376+
('a', [('a', '')]),
377+
('a=', [('a', '')]),
378+
('&a=b', [('a', 'b')]),
379+
('a=a+b&b=b+c', [('a', 'a b'), ('b', 'b c')]),
380+
('a=1&a=2', [('a', '1'), ('a', '2')]),
381+
(';a=b', [(';a', 'b')]),
382+
('a=a+b;b=b+c', [('a', 'a b;b=b c')]),
383+
]
384+
for original, expected in tests:
385+
with self.subTest(original):
386+
result = limited_parse_qsl(original, keep_blank_values=True)
387+
self.assertEqual(result, expected, 'Error parsing %r' % original)
388+
expect_without_blanks = [v for v in expected if len(v[1])]
389+
result = limited_parse_qsl(original, keep_blank_values=False)
390+
self.assertEqual(result, expect_without_blanks, 'Error parsing %r' % original)
391+
392+
def test_parse_qsl_encoding(self):
393+
result = limited_parse_qsl('key=\u0141%E9', encoding='latin-1')
394+
self.assertEqual(result, [('key', '\u0141\xE9')])
395+
result = limited_parse_qsl('key=\u0141%C3%A9', encoding='utf-8')
396+
self.assertEqual(result, [('key', '\u0141\xE9')])
397+
result = limited_parse_qsl('key=\u0141%C3%A9', encoding='ascii')
398+
self.assertEqual(result, [('key', '\u0141\ufffd\ufffd')])
399+
result = limited_parse_qsl('key=\u0141%E9-', encoding='ascii')
400+
self.assertEqual(result, [('key', '\u0141\ufffd-')])
401+
result = limited_parse_qsl('key=\u0141%E9-', encoding='ascii', errors='ignore')
402+
self.assertEqual(result, [('key', '\u0141-')])
403+
404+
def test_parse_qsl_field_limit(self):
405+
with self.assertRaises(TooManyFieldsSent):
406+
limited_parse_qsl('&'.join(['a=a'] * 11), fields_limit=10)
407+
limited_parse_qsl('&'.join(['a=a'] * 10), fields_limit=10)

0 commit comments

Comments
 (0)