8000 [2.2.x] Fixed CVE-2021-23336 -- Fixed web cache poisoning via django.… · django/django@fd6b6af · GitHub
[go: up one dir, main page]

Skip to content
8000

Commit fd6b6af

Browse files
ngnpopecarltongibson
authored andcommitted
[2.2.x] Fixed CVE-2021-23336 -- Fixed web cache poisoning via django.utils.http.limited_parse_qsl().
1 parent 226d831 commit fd6b6af

File tree

6 files changed

+71
-9
lines changed

6 files changed

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

Lines changed: 1 addition & 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+
2.2.19
2829
2.2.18
2930
2.2.17
3031
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: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import unittest
22
from datetime import datetime
33

4+
from django.core.exceptions import TooManyFieldsSent
45
from django.test import SimpleTestCase, ignore_warnings
56
from django.utils.datastructures import MultiValueDict
67
from django.utils.deprecation import RemovedInDjango30Warning
78
from django.utils.http import (
89
base36_to_int, cookie_date, escape_leading_slashes, http_date,
9-
int_to_base36, is_safe_url, is_same_domain, parse_etags, parse_http_date,
10-
quote_etag, urlencode, urlquote, urlquote_plus, urlsafe_base64_decode,
11-
urlsafe_base64_encode, urlunquote, urlunquote_plus,
10+
int_to_base36, is_safe_url, is_same_domain, limited_parse_qsl, parse_etags,
11+
parse_http_date, quote_etag, urlencode, urlquote, urlquote_plus,
12+
urlsafe_base64_decode, urlsafe_base64_encode, urlunquote F438 , urlunquote_plus,
1213
)
1314

1415

@@ -310,3 +311,47 @@ def test(self):
310311
for url, expected in tests:
311312
with self.subTest(url=url):
312313
self.assertEqual(escape_leading_slashes(url), expected)
314+
315+
316+
# Backport of unit tests for urllib.parse.parse_qsl() from Python 3.8.8.
317+
# Copyright (C) 2021 Python Software Foundation (see LICENSE.python).
318+
class ParseQSLBackportTests(unittest.TestCase):
319+
def test_parse_qsl(self):
320+
tests = [
321+
('', []),
322+
('&', []),
323+
('&&', []),
324+
('=', [('', '')]),
325+
('=a', [('', 'a')]),
326+
('a', [('a', '')]),
327+
('a=', [('a', '')]),
328+
('&a=b', [('a', 'b')]),
329+
('a=a+b&b=b+c', [('a', 'a b'), ('b', 'b c')]),
330+
('a=1&a=2', [('a', '1'), ('a', '2')]),
331+
(';a=b', [(';a', 'b')]),
332+
('a=a+b;b=b+c', [('a', 'a b;b=b c')]),
333+
]
334+
for original, expected in tests:
335+
with self.subTest(original):
336+
result = limited_parse_qsl(original, keep_blank_values=True)
337+
self.assertEqual(result, expected, 'Error parsing %r' % original)
338+
expect_without_blanks = [v for v in expected if len(v[1])]
339+
result = limited_parse_qsl(original, keep_blank_values=False)
340+
self.assertEqual(result, expect_without_blanks, 'Error parsing %r' % original)
341+
342+
def test_parse_qsl_encoding(self):
343+
result = limited_parse_qsl('key=\u0141%E9', encoding='latin-1')
344+
self.assertEqual(result, [('key', '\u0141\xE9')])
345+
result = limited_parse_qsl('key=\u0141%C3%A9', encoding='utf-8')
346+
self.assertEqual(result, [('key', '\u0141\xE9')])
347+
result = limited_parse_qsl('key=\u0141%C3%A9', encoding='ascii')
348+
self.assertEqual(result, [('key', '\u0141\ufffd\ufffd')])
349+
result = limited_parse_qsl('key=\u0141%E9-', encoding='ascii')
350+
self.assertEqual(result, [('key', '\u0141\ufffd-')])
351+
result = limited_parse_qsl('key=\u0141%E9-', encoding='ascii', errors='ignore')
352+
self.assertEqual(result, [('key', '\u0141-')])
353+
354+
def test_parse_qsl_field_limit(self):
355+
with self.assertRaises(TooManyFieldsSent):
356+
limited_parse_qsl('&'.join(['a=a'] * 11), fields_limit=10)
357+
limited_parse_qsl('&'.join(['a=a'] * 10), fields_limit=10)

0 commit comments

Comments
 (0)
0