summaryrefslogtreecommitdiffstats
path: root/meta/recipes-devtools/python
diff options
context:
space:
mode:
authorLee Chee Yang <chee.yang.lee@intel.com>2021-06-15 09:21:31 -0700
committerRichard Purdie <richard.purdie@linuxfoundation.org>2021-06-19 16:59:29 +0100
commit9686281c0bb804abead0a8ad94b2af5f771d74b9 (patch)
tree5b2d5e661501eede869ac73189f751b727d9a9fc /meta/recipes-devtools/python
parent7860e4b563b1d928847304e57a6cdc0815134417 (diff)
downloadpoky-9686281c0bb804abead0a8ad94b2af5f771d74b9.tar.gz
python3: fix CVE-2021-23336
The package python/cpython from 0 and before 3.6.13, from 3.7.0 and before 3.7.10, from 3.8.0 and before 3.8.8, from 3.9.0 and before 3.9.2 are vulnerable to Web Cache Poisoning via urllib.parse.parse_qsl and urllib.parse.parse_qs by using a vector called parameter cloaking. When the attacker can separate query parameters using a semicolon (;), they can cause a difference in the interpretation of the request between the proxy (running with default configuration) and the server. This can result in malicious requests being cached as completely safe ones, as the proxy would usually not see the semicolon as a separator, and therefore would not include it in a cache key of an unkeyed parameter. References: https://nvd.nist.gov/vuln/detail/CVE-2021-23336 https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-23336 (From OE-Core rev: 8a59c47ce4c101b2470a06ecf101ca5ab7d1f82e) Signed-off-by: Lee Chee Yang <chee.yang.lee@intel.com> Signed-off-by: Tim Orling <timothy.t.orling@intel.com> Signed-off-by: Steve Sakoman <steve@sakoman.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/recipes-devtools/python')
-rw-r--r--meta/recipes-devtools/python/python3/CVE-2021-23336.patch530
-rw-r--r--meta/recipes-devtools/python/python3_3.8.2.bb1
2 files changed, 531 insertions, 0 deletions
diff --git a/meta/recipes-devtools/python/python3/CVE-2021-23336.patch b/meta/recipes-devtools/python/python3/CVE-2021-23336.patch
new file mode 100644
index 0000000000..2a885b9d37
--- /dev/null
+++ b/meta/recipes-devtools/python/python3/CVE-2021-23336.patch
@@ -0,0 +1,530 @@
1From 3ab6f812653e79d008d5eba31dc25d34f3ca7170 Mon Sep 17 00:00:00 2001
2From: Senthil Kumaran <senthil@uthcode.com>
3Date: Mon, 15 Feb 2021 10:15:02 -0800
4Subject: [PATCH] bpo-42967: only use '&' as a query string separator
5MIME-Version: 1.0
6Content-Type: text/plain; charset=UTF-8
7Content-Transfer-Encoding: 8bit
8
9 (GH-24297) (#24529)
10MIME-Version: 1.0
11Content-Type: text/plain; charset=UTF-8
12Content-Transfer-Encoding: 8bit
13
14* bpo-42967: only use '&' as a query string separator (#24297)
15
16bpo-42967: [security] Address a web cache-poisoning issue reported in
17urllib.parse.parse_qsl().
18
19urllib.parse will only us "&" as query string separator by default
20instead of both ";" and "&" as allowed in earlier versions. An optional
21argument seperator with default value "&" is added to specify the
22separator.
23
24Co-authored-by: Éric Araujo <merwok@netwok.org>
25Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
26Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com>
27Co-authored-by: Éric Araujo <merwok@netwok.org>
28(cherry picked from commit fcbe0cb04d35189401c0c880ebfb4311e952d776)
29
30* [3.8] bpo-42967: only use '&' as a query string separator (GH-24297)
31
32bpo-42967: [security] Address a web cache-poisoning issue reported in urllib.parse.parse_qsl().
33
34urllib.parse will only us "&" as query string separator by default instead of both ";" and "&" as allowed in earlier versions. An optional argument seperator with default value "&" is added to specify the separator.
35
36Co-authored-by: Éric Araujo <merwok@netwok.org>
37Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
38Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com>
39Co-authored-by: Éric Araujo <merwok@netwok.org>.
40(cherry picked from commit fcbe0cb04d35189401c0c880ebfb4311e952d776)
41
42Co-authored-by: Adam Goldschmidt <adamgold7@gmail.com>
43
44* Update correct version information.
45
46* fix docs and make logic clearer
47
48Co-authored-by: Adam Goldschmidt <adamgold7@gmail.com>
49Co-authored-by: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com>
50
51Upstream-Status: Backport [https://github.com/python/cpython/commit/e3110c3cfbb7daa690d54d0eff6c264c870a71bf]
52CVE: CVE-2020-23336
53Signed-off-by: Chee Yang Lee <chee.yang.lee@intel.com>
54
55---
56 Doc/library/cgi.rst | 11 ++-
57 Doc/library/urllib.parse.rst | 22 +++++-
58 Doc/whatsnew/3.6.rst | 13 ++++
59 Doc/whatsnew/3.7.rst | 13 ++++
60 Lib/cgi.py | 23 ++++---
61 Lib/test/test_cgi.py | 29 ++++++--
62 Lib/test/test_urlparse.py | 68 +++++++++++++------
63 Lib/urllib/parse.py | 19 ++++--
64 .../2021-02-14-15-59-16.bpo-42967.YApqDS.rst | 1 +
65 9 files changed, 153 insertions(+), 46 deletions(-)
66 create mode 100644 Misc/NEWS.d/next/Security/2021-02-14-15-59-16.bpo-42967.YApqDS.rst
67
68diff --git a/Doc/library/cgi.rst b/Doc/library/cgi.rst
69index 4048592..880074b 100644
70--- a/Doc/library/cgi.rst
71+++ b/Doc/library/cgi.rst
72@@ -277,14 +277,16 @@ These are useful if you want more control, or if you want to employ some of the
73 algorithms implemented in this module in other circumstances.
74
75
76-.. function:: parse(fp=None, environ=os.environ, keep_blank_values=False, strict_parsing=False)
77+.. function:: parse(fp=None, environ=os.environ, keep_blank_values=False, strict_parsing=False, separator="&")
78
79 Parse a query in the environment or from a file (the file defaults to
80- ``sys.stdin``). The *keep_blank_values* and *strict_parsing* parameters are
81+ ``sys.stdin``). The *keep_blank_values*, *strict_parsing* and *separator* parameters are
82 passed to :func:`urllib.parse.parse_qs` unchanged.
83
84+ .. versionchanged:: 3.8.8
85+ Added the *separator* parameter.
86
87-.. function:: parse_multipart(fp, pdict, encoding="utf-8", errors="replace")
88+.. function:: parse_multipart(fp, pdict, encoding="utf-8", errors="replace", separator="&")
89
90 Parse input of type :mimetype:`multipart/form-data` (for file uploads).
91 Arguments are *fp* for the input file, *pdict* for a dictionary containing
92@@ -303,6 +305,9 @@ algorithms implemented in this module in other circumstances.
93 Added the *encoding* and *errors* parameters. For non-file fields, the
94 value is now a list of strings, not bytes.
95
96+ .. versionchanged:: 3.8.8
97+ Added the *separator* parameter.
98+
99
100 .. function:: parse_header(string)
101
102diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst
103index 52f98ef..45ca03a 100644
104--- a/Doc/library/urllib.parse.rst
105+++ b/Doc/library/urllib.parse.rst
106@@ -165,7 +165,7 @@ or on combining URL components into a URL string.
107 now raise :exc:`ValueError`.
108
109
110-.. function:: parse_qs(qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8', errors='replace', max_num_fields=None)
111+.. function:: parse_qs(qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8', errors='replace', max_num_fields=None, separator='&')
112
113 Parse a query string given as a string argument (data of type
114 :mimetype:`application/x-www-form-urlencoded`). Data are returned as a
115@@ -190,6 +190,9 @@ or on combining URL components into a URL string.
116 read. If set, then throws a :exc:`ValueError` if there are more than
117 *max_num_fields* fields read.
118
119+ The optional argument *separator* is the symbol to use for separating the
120+ query arguments. It defaults to ``&``.
121+
122 Use the :func:`urllib.parse.urlencode` function (with the ``doseq``
123 parameter set to ``True``) to convert such dictionaries into query
124 strings.
125@@ -201,8 +204,14 @@ or on combining URL components into a URL string.
126 .. versionchanged:: 3.8
127 Added *max_num_fields* parameter.
128
129+ .. versionchanged:: 3.8.8
130+ Added *separator* parameter with the default value of ``&``. Python
131+ versions earlier than Python 3.8.8 allowed using both ``;`` and ``&`` as
132+ query parameter separator. This has been changed to allow only a single
133+ separator key, with ``&`` as the default separator.
134+
135
136-.. function:: parse_qsl(qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8', errors='replace', max_num_fields=None)
137+.. function:: parse_qsl(qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8', errors='replace', max_num_fields=None, separator='&')
138
139 Parse a query string given as a string argument (data of type
140 :mimetype:`application/x-www-form-urlencoded`). Data are returned as a list of
141@@ -226,6 +235,9 @@ or on combining URL components into a URL string.
142 read. If set, then throws a :exc:`ValueError` if there are more than
143 *max_num_fields* fields read.
144
145+ The optional argument *separator* is the symbol to use for separating the
146+ query arguments. It defaults to ``&``.
147+
148 Use the :func:`urllib.parse.urlencode` function to convert such lists of pairs into
149 query strings.
150
151@@ -235,6 +247,12 @@ or on combining URL components into a URL string.
152 .. versionchanged:: 3.8
153 Added *max_num_fields* parameter.
154
155+ .. versionchanged:: 3.8.8
156+ Added *separator* parameter with the default value of ``&``. Python
157+ versions earlier than Python 3.8.8 allowed using both ``;`` and ``&`` as
158+ query parameter separator. This has been changed to allow only a single
159+ separator key, with ``&`` as the default separator.
160+
161
162 .. function:: urlunparse(parts)
163
164diff --git a/Doc/whatsnew/3.6.rst b/Doc/whatsnew/3.6.rst
165index 04c1f7e..4409a3a 100644
166--- a/Doc/whatsnew/3.6.rst
167+++ b/Doc/whatsnew/3.6.rst
168@@ -2443,3 +2443,16 @@ because of the behavior of the socket option ``SO_REUSEADDR`` in UDP. For more
169 details, see the documentation for ``loop.create_datagram_endpoint()``.
170 (Contributed by Kyle Stanley, Antoine Pitrou, and Yury Selivanov in
171 :issue:`37228`.)
172+
173+Notable changes in Python 3.6.13
174+================================
175+
176+Earlier Python versions allowed using both ``;`` and ``&`` as
177+query parameter separators in :func:`urllib.parse.parse_qs` and
178+:func:`urllib.parse.parse_qsl`. Due to security concerns, and to conform with
179+newer W3C recommendations, this has been changed to allow only a single
180+separator key, with ``&`` as the default. This change also affects
181+:func:`cgi.parse` and :func:`cgi.parse_multipart` as they use the affected
182+functions internally. For more details, please see their respective
183+documentation.
184+(Contributed by Adam Goldschmidt, Senthil Kumaran and Ken Jin in :issue:`42967`.)
185diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst
186index b9b5021..8f47a9f 100644
187--- a/Doc/whatsnew/3.7.rst
188+++ b/Doc/whatsnew/3.7.rst
189@@ -2556,3 +2556,16 @@ because of the behavior of the socket option ``SO_REUSEADDR`` in UDP. For more
190 details, see the documentation for ``loop.create_datagram_endpoint()``.
191 (Contributed by Kyle Stanley, Antoine Pitrou, and Yury Selivanov in
192 :issue:`37228`.)
193+
194+Notable changes in Python 3.7.10
195+================================
196+
197+Earlier Python versions allowed using both ``;`` and ``&`` as
198+query parameter separators in :func:`urllib.parse.parse_qs` and
199+:func:`urllib.parse.parse_qsl`. Due to security concerns, and to conform with
200+newer W3C recommendations, this has been changed to allow only a single
201+separator key, with ``&`` as the default. This change also affects
202+:func:`cgi.parse` and :func:`cgi.parse_multipart` as they use the affected
203+functions internally. For more details, please see their respective
204+documentation.
205+(Contributed by Adam Goldschmidt, Senthil Kumaran and Ken Jin in :issue:`42967`.)
206diff --git a/Lib/cgi.py b/Lib/cgi.py
207index 5ace46a..13255a9 100755
208--- a/Lib/cgi.py
209+++ b/Lib/cgi.py
210@@ -106,7 +106,8 @@ log = initlog # The current logging function
211 # 0 ==> unlimited input
212 maxlen = 0
213
214-def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
215+def parse(fp=None, environ=os.environ, keep_blank_values=0,
216+ strict_parsing=0, separator='&'):
217 """Parse a query in the environment or from a file (default stdin)
218
219 Arguments, all optional:
220@@ -125,6 +126,9 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
221 strict_parsing: flag indicating what to do with parsing errors.
222 If false (the default), errors are silently ignored.
223 If true, errors raise a ValueError exception.
224+
225+ separator: str. The symbol to use for separating the query arguments.
226+ Defaults to &.
227 """
228 if fp is None:
229 fp = sys.stdin
230@@ -145,7 +149,7 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
231 if environ['REQUEST_METHOD'] == 'POST':
232 ctype, pdict = parse_header(environ['CONTENT_TYPE'])
233 if ctype == 'multipart/form-data':
234- return parse_multipart(fp, pdict)
235+ return parse_multipart(fp, pdict, separator=separator)
236 elif ctype == 'application/x-www-form-urlencoded':
237 clength = int(environ['CONTENT_LENGTH'])
238 if maxlen and clength > maxlen:
239@@ -169,10 +173,10 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
240 qs = ""
241 environ['QUERY_STRING'] = qs # XXX Shouldn't, really
242 return urllib.parse.parse_qs(qs, keep_blank_values, strict_parsing,
243- encoding=encoding)
244+ encoding=encoding, separator=separator)
245
246
247-def parse_multipart(fp, pdict, encoding="utf-8", errors="replace"):
248+def parse_multipart(fp, pdict, encoding="utf-8", errors="replace", separator='&'):
249 """Parse multipart input.
250
251 Arguments:
252@@ -193,7 +197,7 @@ def parse_multipart(fp, pdict, encoding="utf-8", errors="replace"):
253 headers.set_type(ctype)
254 headers['Content-Length'] = pdict['CONTENT-LENGTH']
255 fs = FieldStorage(fp, headers=headers, encoding=encoding, errors=errors,
256- environ={'REQUEST_METHOD': 'POST'})
257+ environ={'REQUEST_METHOD': 'POST'}, separator=separator)
258 return {k: fs.getlist(k) for k in fs}
259
260 def _parseparam(s):
261@@ -303,7 +307,7 @@ class FieldStorage:
262 def __init__(self, fp=None, headers=None, outerboundary=b'',
263 environ=os.environ, keep_blank_values=0, strict_parsing=0,
264 limit=None, encoding='utf-8', errors='replace',
265- max_num_fields=None):
266+ max_num_fields=None, separator='&'):
267 """Constructor. Read multipart/* until last part.
268
269 Arguments, all optional:
270@@ -351,6 +355,7 @@ class FieldStorage:
271 self.keep_blank_values = keep_blank_values
272 self.strict_parsing = strict_parsing
273 self.max_num_fields = max_num_fields
274+ self.separator = separator
275 if 'REQUEST_METHOD' in environ:
276 method = environ['REQUEST_METHOD'].upper()
277 self.qs_on_post = None
278@@ -577,7 +582,7 @@ class FieldStorage:
279 query = urllib.parse.parse_qsl(
280 qs, self.keep_blank_values, self.strict_parsing,
281 encoding=self.encoding, errors=self.errors,
282- max_num_fields=self.max_num_fields)
283+ max_num_fields=self.max_num_fields, separator=self.separator)
284 self.list = [MiniFieldStorage(key, value) for key, value in query]
285 self.skip_lines()
286
287@@ -593,7 +598,7 @@ class FieldStorage:
288 query = urllib.parse.parse_qsl(
289 self.qs_on_post, self.keep_blank_values, self.strict_parsing,
290 encoding=self.encoding, errors=self.errors,
291- max_num_fields=self.max_num_fields)
292+ max_num_fields=self.max_num_fields, separator=self.separator)
293 self.list.extend(MiniFieldStorage(key, value) for key, value in query)
294
295 klass = self.FieldStorageClass or self.__class__
296@@ -637,7 +642,7 @@ class FieldStorage:
297 else self.limit - self.bytes_read
298 part = klass(self.fp, headers, ib, environ, keep_blank_values,
299 strict_parsing, limit,
300- self.encoding, self.errors, max_num_fields)
301+ self.encoding, self.errors, max_num_fields, self.separator)
302
303 if max_num_fields is not None:
304 max_num_fields -= 1
305diff --git a/Lib/test/test_cgi.py b/Lib/test/test_cgi.py
306index ab86771..bda03ee 100644
307--- a/Lib/test/test_cgi.py
308+++ b/Lib/test/test_cgi.py
309@@ -53,12 +53,9 @@ parse_strict_test_cases = [
310 ("", ValueError("bad query field: ''")),
311 ("&", ValueError("bad query field: ''")),
312 ("&&", ValueError("bad query field: ''")),
313- (";", ValueError("bad query field: ''")),
314- (";&;", ValueError("bad query field: ''")),
315 # Should the next few really be valid?
316 ("=", {}),
317 ("=&=", {}),
318- ("=;=", {}),
319 # This rest seem to make sense
320 ("=a", {'': ['a']}),
321 ("&=a", ValueError("bad query field: ''")),
322@@ -73,8 +70,6 @@ parse_strict_test_cases = [
323 ("a=a+b&b=b+c", {'a': ['a b'], 'b': ['b c']}),
324 ("a=a+b&a=b+a", {'a': ['a b', 'b a']}),
325 ("x=1&y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
326- ("x=1;y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
327- ("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
328 ("Hbc5161168c542333633315dee1182227:key_store_seqid=400006&cuyer=r&view=bustomer&order_id=0bb2e248638833d48cb7fed300000f1b&expire=964546263&lobale=en-US&kid=130003.300038&ss=env",
329 {'Hbc5161168c542333633315dee1182227:key_store_seqid': ['400006'],
330 'cuyer': ['r'],
331@@ -187,6 +182,30 @@ Content-Length: 3
332 else:
333 self.assertEqual(fs.getvalue(key), expect_val[0])
334
335+ def test_separator(self):
336+ parse_semicolon = [
337+ ("x=1;y=2.0", {'x': ['1'], 'y': ['2.0']}),
338+ ("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
339+ (";", ValueError("bad query field: ''")),
340+ (";;", ValueError("bad query field: ''")),
341+ ("=;a", ValueError("bad query field: 'a'")),
342+ (";b=a", ValueError("bad query field: ''")),
343+ ("b;=a", ValueError("bad query field: 'b'")),
344+ ("a=a+b;b=b+c", {'a': ['a b'], 'b': ['b c']}),
345+ ("a=a+b;a=b+a", {'a': ['a b', 'b a']}),
346+ ]
347+ for orig, expect in parse_semicolon:
348+ env = {'QUERY_STRING': orig}
349+ fs = cgi.FieldStorage(separator=';', environ=env)
350+ if isinstance(expect, dict):
351+ for key in expect.keys():
352+ expect_val = expect[key]
353+ self.assertIn(key, fs)
354+ if len(expect_val) > 1:
355+ self.assertEqual(fs.getvalue(key), expect_val)
356+ else:
357+ self.assertEqual(fs.getvalue(key), expect_val[0])
358+
359 def test_log(self):
360 cgi.log("Testing")
361
362diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py
363index 4ae6ed3..90c8d69 100644
364--- a/Lib/test/test_urlparse.py
365+++ b/Lib/test/test_urlparse.py
366@@ -32,16 +32,10 @@ parse_qsl_test_cases = [
367 (b"&a=b", [(b'a', b'b')]),
368 (b"a=a+b&b=b+c", [(b'a', b'a b'), (b'b', b'b c')]),
369 (b"a=1&a=2", [(b'a', b'1'), (b'a', b'2')]),
370- (";", []),
371- (";;", []),
372- (";a=b", [('a', 'b')]),
373- ("a=a+b;b=b+c", [('a', 'a b'), ('b', 'b c')]),
374- ("a=1;a=2", [('a', '1'), ('a', '2')]),
375- (b";", []),
376- (b";;", []),
377- (b";a=b", [(b'a', b'b')]),
378- (b"a=a+b;b=b+c", [(b'a', b'a b'), (b'b', b'b c')]),
379- (b"a=1;a=2", [(b'a', b'1'), (b'a', b'2')]),
380+ (";a=b", [(';a', 'b')]),
381+ ("a=a+b;b=b+c", [('a', 'a b;b=b c')]),
382+ (b";a=b", [(b';a', b'b')]),
383+ (b"a=a+b;b=b+c", [(b'a', b'a b;b=b c')]),
384 ]
385
386 # Each parse_qs testcase is a two-tuple that contains
387@@ -68,16 +62,10 @@ parse_qs_test_cases = [
388 (b"&a=b", {b'a': [b'b']}),
389 (b"a=a+b&b=b+c", {b'a': [b'a b'], b'b': [b'b c']}),
390 (b"a=1&a=2", {b'a': [b'1', 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', '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'2']}),
401+ (";a=b", {';a': ['b']}),
402+ ("a=a+b;b=b+c", {'a': ['a b;b=b c']}),
403+ (b";a=b", {b';a': [b'b']}),
404+ (b"a=a+b;b=b+c", {b'a':[ b'a b;b=b c']}),
405 ]
406
407 class UrlParseTestCase(unittest.TestCase):
408@@ -884,10 +872,46 @@ class UrlParseTestCase(unittest.TestCase):
409 def test_parse_qsl_max_num_fields(self):
410 with self.assertRaises(ValueError):
411 urllib.parse.parse_qs('&'.join(['a=a']*11), max_num_fields=10)
412- with self.assertRaises(ValueError):
413- urllib.parse.parse_qs(';'.join(['a=a']*11), max_num_fields=10)
414 urllib.parse.parse_qs('&'.join(['a=a']*10), max_num_fields=10)
415
416+ def test_parse_qs_separator(self):
417+ parse_qs_semicolon_cases = [
418+ (";", {}),
419+ (";;", {}),
420+ (";a=b", {'a': ['b']}),
421+ ("a=a+b;b=b+c", {'a': ['a b'], 'b': ['b c']}),
422+ ("a=1;a=2", {'a': ['1', '2']}),
423+ (b";", {}),
424+ (b";;", {}),
425+ (b";a=b", {b'a': [b'b']}),
426+ (b"a=a+b;b=b+c", {b'a': [b'a b'], b'b': [b'b c']}),
427+ (b"a=1;a=2", {b'a': [b'1', b'2']}),
428+ ]
429+ for orig, expect in parse_qs_semicolon_cases:
430+ with self.subTest(f"Original: {orig!r}, Expected: {expect!r}"):
431+ result = urllib.parse.parse_qs(orig, separator=';')
432+ self.assertEqual(result, expect, "Error parsing %r" % orig)
433+
434+
435+ def test_parse_qsl_separator(self):
436+ parse_qsl_semicolon_cases = [
437+ (";", []),
438+ (";;", []),
439+ (";a=b", [('a', 'b')]),
440+ ("a=a+b;b=b+c", [('a', 'a b'), ('b', 'b c')]),
441+ ("a=1;a=2", [('a', '1'), ('a', '2')]),
442+ (b";", []),
443+ (b";;", []),
444+ (b";a=b", [(b'a', b'b')]),
445+ (b"a=a+b;b=b+c", [(b'a', b'a b'), (b'b', b'b c')]),
446+ (b"a=1;a=2", [(b'a', b'1'), (b'a', b'2')]),
447+ ]
448+ for orig, expect in parse_qsl_semicolon_cases:
449+ with self.subTest(f"Original: {orig!r}, Expected: {expect!r}"):
450+ result = urllib.parse.parse_qsl(orig, separator=';')
451+ self.assertEqual(result, expect, "Error parsing %r" % orig)
452+
453+
454 def test_urlencode_sequences(self):
455 # Other tests incidentally urlencode things; test non-covered cases:
456 # Sequence and object values.
457diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py
458index e2b6f13..5a3e847 100644
459--- a/Lib/urllib/parse.py
460+++ b/Lib/urllib/parse.py
461@@ -648,7 +648,7 @@ def unquote(string, encoding='utf-8', errors='replace'):
462
463
464 def parse_qs(qs, keep_blank_values=False, strict_parsing=False,
465- encoding='utf-8', errors='replace', max_num_fields=None):
466+ encoding='utf-8', errors='replace', max_num_fields=None, separator='&'):
467 """Parse a query given as a string argument.
468
469 Arguments:
470@@ -672,12 +672,15 @@ def parse_qs(qs, keep_blank_values=False, strict_parsing=False,
471 max_num_fields: int. If set, then throws a ValueError if there
472 are more than n fields read by parse_qsl().
473
474+ separator: str. The symbol to use for separating the query arguments.
475+ Defaults to &.
476+
477 Returns a dictionary.
478 """
479 parsed_result = {}
480 pairs = parse_qsl(qs, keep_blank_values, strict_parsing,
481 encoding=encoding, errors=errors,
482- max_num_fields=max_num_fields)
483+ max_num_fields=max_num_fields, separator=separator)
484 for name, value in pairs:
485 if name in parsed_result:
486 parsed_result[name].append(value)
487@@ -687,7 +690,7 @@ def parse_qs(qs, keep_blank_values=False, strict_parsing=False,
488
489
490 def parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
491- encoding='utf-8', errors='replace', max_num_fields=None):
492+ encoding='utf-8', errors='replace', max_num_fields=None, separator='&'):
493 """Parse a query given as a string argument.
494
495 Arguments:
496@@ -710,19 +713,25 @@ def parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
497 max_num_fields: int. If set, then throws a ValueError
498 if there are more than n fields read by parse_qsl().
499
500+ separator: str. The symbol to use for separating the query arguments.
501+ Defaults to &.
502+
503 Returns a list, as G-d intended.
504 """
505 qs, _coerce_result = _coerce_args(qs)
506
507+ if not separator or (not isinstance(separator, (str, bytes))):
508+ raise ValueError("Separator must be of type string or bytes.")
509+
510 # If max_num_fields is defined then check that the number of fields
511 # is less than max_num_fields. This prevents a memory exhaustion DOS
512 # attack via post bodies with many fields.
513 if max_num_fields is not None:
514- num_fields = 1 + qs.count('&') + qs.count(';')
515+ num_fields = 1 + qs.count(separator)
516 if max_num_fields < num_fields:
517 raise ValueError('Max number of fields exceeded')
518
519- pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
520+ pairs = [s1 for s1 in qs.split(separator)]
521 r = []
522 for name_value in pairs:
523 if not name_value and not strict_parsing:
524diff --git a/Misc/NEWS.d/next/Security/2021-02-14-15-59-16.bpo-42967.YApqDS.rst b/Misc/NEWS.d/next/Security/2021-02-14-15-59-16.bpo-42967.YApqDS.rst
525new file mode 100644
526index 0000000..f08489b
527--- /dev/null
528+++ b/Misc/NEWS.d/next/Security/2021-02-14-15-59-16.bpo-42967.YApqDS.rst
529@@ -0,0 +1 @@
530+Fix web cache poisoning vulnerability by defaulting the query args separator to ``&``, and allowing the user to choose a custom separator.
diff --git a/meta/recipes-devtools/python/python3_3.8.2.bb b/meta/recipes-devtools/python/python3_3.8.2.bb
index 072ce97472..762e9444b8 100644
--- a/meta/recipes-devtools/python/python3_3.8.2.bb
+++ b/meta/recipes-devtools/python/python3_3.8.2.bb
@@ -39,6 +39,7 @@ SRC_URI = "http://www.python.org/ftp/python/${PV}/Python-${PV}.tar.xz \
39 file://CVE-2020-26116.patch \ 39 file://CVE-2020-26116.patch \
40 file://CVE-2020-27619.patch \ 40 file://CVE-2020-27619.patch \
41 file://CVE-2021-3177.patch \ 41 file://CVE-2021-3177.patch \
42 file://CVE-2021-23336.patch \
42 " 43 "
43 44
44SRC_URI_append_class-native = " \ 45SRC_URI_append_class-native = " \