티스토리 수익 글 보기

티스토리 수익 글 보기

[3.1.x] Fixed CVE-2021-23336 — Fixed web cache poisoning via django.… · django/django@8f6d431 · GitHub
Skip to content
/ django Public

Commit 8f6d431

Browse files
ngnpopecarltongibson
authored andcommitted
[3.1.x] Fixed CVE-2021-23336 — Fixed web cache poisoning via django.utils.http.limited_parse_qsl().
1 parent 536d117 commit 8f6d431

File tree

8 files changed

+100
11
lines changed

8 files changed

+100
11
lines changed

django/utils/http.py

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

45-
FIELDS_MATCH = _lazy_re_compile('[&;]')
45+
FIELDS_MATCH = _lazy_re_compile('&')
4646

4747

4848
@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/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
@@ -39,6 +39,7 @@ versions of the documentation contain the release notes for any later releases.
3939
.. toctree::
4040
:maxdepth: 1
4141

42+
3.0.13
4243
3.0.12
4344
3.0.11
4445
3.0.10
@@ -58,6 +59,7 @@ versions of the documentation contain the release notes for any later releases.
5859
.. toctree::
5960
:maxdepth: 1
6061

62+
2.2.19
6163
2.2.18
6264
2.2.17
6365
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)