[packages/python] - backport security fixes from Gentoo/Debian/RHEL; rel 11
arekm
arekm at pld-linux.org
Tue Aug 18 14:49:33 CEST 2026
commit d265a4e262afa1319ff48749f6dd0d4a06fdfb2e
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date: Tue Aug 18 14:48:06 2026 +0200
- backport security fixes from Gentoo/Debian/RHEL; rel 11
CVE-2015-20107.patch | 440 +++++++++++++++++++++++++++++++++
CVE-2019-20907.patch | 39 +++
CVE-2020-26116.patch | 93 +++++++
CVE-2020-27619.patch | 119 +++++++++
CVE-2020-8492.patch | 213 ++++++++++++++++
CVE-2021-23336.patch | 390 +++++++++++++++++++++++++++++
CVE-2021-3177.patch | 181 ++++++++++++++
CVE-2021-3733.patch | 39 +++
CVE-2021-3737.patch | 83 +++++++
CVE-2021-4189.patch | 133 ++++++++++
CVE-2022-0391.patch | 151 +++++++++++
CVE-2022-45061.patch | 118 +++++++++
CVE-2022-48560.patch | 155 ++++++++++++
CVE-2022-48565.patch | 91 +++++++
CVE-2022-48566.patch | 48 ++++
CVE-2023-24329.patch | 264 ++++++++++++++++++++
CVE-2023-40217.patch | 321 ++++++++++++++++++++++++
python-bpo-42278-pydoc-mktemp.patch | 59 +++++
python-bpo-43124-smtplib-crlf.patch | 180 ++++++++++++++
python-bpo-46756-urllib-auth.patch | 77 ++++++
python-configure-c99.patch | 52 ++++
python-expat-2.4.5-tests.patch | 86 +++++++
python-test-support-unlink-errno.patch | 21 ++
python.spec | 53 +++-
24 files changed, 3405 insertions(+), 1 deletion(-)
---
diff --git a/python.spec b/python.spec
index 09c579d..bed0294 100644
--- a/python.spec
+++ b/python.spec
@@ -24,6 +24,9 @@
# tests which fail because of some unknown/unresolved reason (this list should be ideally just %{nil})
%define broken_tests test_doctest test_pydoc test_distutils test_gdb
+# C sources predate C23, which GCC 15 defaults to (asdl.h: typedef enum {false, true} bool)
+%define specflags -std=gnu17
+
%define py_ver 2.7
%define py_prefix %{_prefix}
%define py_libdir %{py_prefix}/%{_lib}/python%{py_ver}
@@ -42,7 +45,7 @@ Summary(tr.UTF-8): X arayüzlü, yüksek düzeyli, kabuk yorumlayıcı dili
Summary(uk.UTF-8): Мова програмування дуже високого рівня з X-інтерфейсом
Name: python
Version: %{py_ver}.18
-Release: 10
+Release: 11
Epoch: 1
License: PSF
Group: Development/Languages/Python
@@ -66,6 +69,31 @@ Patch9: https://bugs.python.org/file21896/nonexistent_user.patch
Patch10: %{name}-BLDLIBRARY.patch
Patch11: openssl3.patch
Patch12: gcc14.patch
+# Post-2.7.18 backports; upstream CPython 2.7 has had no commits since 2020-04-19.
+# Sources: Gentoo python-gentoo-patches-2.7.18_p16, Debian 2.7.18-8+deb11u1, RHEL python2-2.7.18-17.
+Patch13: CVE-2019-20907.patch
+Patch14: CVE-2020-8492.patch
+Patch15: CVE-2020-26116.patch
+Patch16: CVE-2022-48565.patch
+Patch17: CVE-2020-27619.patch
+Patch18: CVE-2022-48566.patch
+Patch19: CVE-2021-3177.patch
+Patch20: CVE-2021-23336.patch
+Patch21: CVE-2021-4189.patch
+Patch22: CVE-2021-3733.patch
+Patch23: CVE-2021-3737.patch
+Patch24: %{name}-bpo-43124-smtplib-crlf.patch
+Patch25: %{name}-bpo-42278-pydoc-mktemp.patch
+Patch26: %{name}-bpo-46756-urllib-auth.patch
+Patch27: CVE-2022-0391.patch
+Patch28: CVE-2022-45061.patch
+Patch29: CVE-2022-48560.patch
+Patch30: CVE-2023-24329.patch
+Patch31: CVE-2023-40217.patch
+Patch32: CVE-2015-20107.patch
+Patch33: %{name}-expat-2.4.5-tests.patch
+Patch34: %{name}-configure-c99.patch
+Patch35: %{name}-test-support-unlink-errno.patch
URL: https://www.python.org/
BuildRequires: autoconf >= 2.65
BuildRequires: automake
@@ -601,6 +629,29 @@ napisanego w Pythonie.
%patch -P10 -p1
%patch -P11 -p1
%patch -P12 -p1
+%patch -P13 -p1
+%patch -P14 -p1
+%patch -P15 -p1
+%patch -P16 -p1
+%patch -P17 -p1
+%patch -P18 -p1
+%patch -P19 -p1
+%patch -P20 -p1
+%patch -P21 -p1
+%patch -P22 -p1
+%patch -P23 -p1
+%patch -P24 -p1
+%patch -P25 -p1
+%patch -P26 -p1
+%patch -P27 -p1
+%patch -P28 -p1
+%patch -P29 -p1
+%patch -P30 -p1
+%patch -P31 -p1
+%patch -P32 -p1
+%patch -P33 -p1
+%patch -P34 -p1
+%patch -P35 -p1
tar xjf %{SOURCE1}
diff --git a/CVE-2015-20107.patch b/CVE-2015-20107.patch
new file mode 100644
index 0000000..edc1843
--- /dev/null
+++ b/CVE-2015-20107.patch
@@ -0,0 +1,440 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Petr Viktorin <encukou at gmail.com>
+Date: Fri, 3 Jun 2022 11:43:35 +0200
+Subject: [PATCH] 00382-cve-2015-20107.patch
+
+00382 #
+Make mailcap refuse to match unsafe filenames/types/params (GH-91993)
+
+Upstream: https://github.com/python/cpython/issues/68966
+
+Tracker bug: https://bugzilla.redhat.com/show_bug.cgi?id=2075390
+
+Backported from python3.
+---
+ Doc/library/mailcap.rst | 12 +
+ Lib/mailcap.py | 29 +-
+ Lib/test/mailcap.txt | 39 +++
+ Lib/test/test_mailcap.py | 259 ++++++++++++++++++
+ ...2-04-27-18-25-30.gh-issue-68966.gjS8zs.rst | 4 +
+ 5 files changed, 341 insertions(+), 2 deletions(-)
+ create mode 100644 Lib/test/mailcap.txt
+ create mode 100644 Lib/test/test_mailcap.py
+ create mode 100644 Misc/NEWS.d/next/Security/2022-04-27-18-25-30.gh-issue-68966.gjS8zs.rst
+
+diff --git a/Doc/library/mailcap.rst b/Doc/library/mailcap.rst
+index 750d085796f..5f75ee6086e 100644
+--- a/Doc/library/mailcap.rst
++++ b/Doc/library/mailcap.rst
+@@ -54,6 +54,18 @@ standard. However, mailcap files are supported on most Unix systems.
+ use) to determine whether or not the mailcap line applies. :func:`findmatch`
+ will automatically check such conditions and skip the entry if the check fails.
+
++ .. versionchanged:: 3.11
++
++ To prevent security issues with shell metacharacters (symbols that have
++ special effects in a shell command line), ``findmatch`` will refuse
++ to inject ASCII characters other than alphanumerics and ``@+=:,./-_``
++ into the returned command line.
++
++ If a disallowed character appears in *filename*, ``findmatch`` will always
++ return ``(None, None)`` as if no entry was found.
++ If such a character appears elsewhere (a value in *plist* or in *MIMEtype*),
++ ``findmatch`` will ignore all mailcap entries which use that value.
++ A :mod:`warning <warnings>` will be raised in either case.
+
+ .. function:: getcaps()
+
+diff --git a/Lib/mailcap.py b/Lib/mailcap.py
+index 04077ba0db2..1108b447b1d 100644
+--- a/Lib/mailcap.py
++++ b/Lib/mailcap.py
+@@ -1,9 +1,18 @@
+ """Mailcap file handling. See RFC 1524."""
+
+ import os
++import warnings
++import re
+
+ __all__ = ["getcaps","findmatch"]
+
++
++_find_unsafe = re.compile(r'[^\xa1-\xff\w at +=:,./-]').search
++
++class UnsafeMailcapInput(Warning):
++ """Warning raised when refusing unsafe input"""
++
++
+ # Part 1: top-level interface.
+
+ def getcaps():
+@@ -144,15 +153,22 @@ def findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]):
+ entry to use.
+
+ """
++ if _find_unsafe(filename):
++ msg = "Refusing to use mailcap with filename %r. Use a safe temporary filename." % (filename,)
++ warnings.warn(msg, UnsafeMailcapInput)
++ return None, None
+ entries = lookup(caps, MIMEtype, key)
+ # XXX This code should somehow check for the needsterminal flag.
+ for e in entries:
+ if 'test' in e:
+ test = subst(e['test'], filename, plist)
++ if test is None:
++ continue
+ if test and os.system(test) != 0:
+ continue
+ command = subst(e[key], MIMEtype, filename, plist)
+- return command, e
++ if command is not None:
++ return command, e
+ return None, None
+
+ def lookup(caps, MIMEtype, key=None):
+@@ -184,6 +200,10 @@ def subst(field, MIMEtype, filename, plist=[]):
+ elif c == 's':
+ res = res + filename
+ elif c == 't':
++ if _find_unsafe(MIMEtype):
++ msg = "Refusing to substitute MIME type %r into a shell command." % (MIMEtype,)
++ warnings.warn(msg, UnsafeMailcapInput)
++ return None
+ res = res + MIMEtype
+ elif c == '{':
+ start = i
+@@ -191,7 +211,12 @@ def subst(field, MIMEtype, filename, plist=[]):
+ i = i+1
+ name = field[start:i]
+ i = i+1
+- res = res + findparam(name, plist)
++ param = findparam(name, plist)
++ if _find_unsafe(param):
++ msg = "Refusing to substitute parameter %r (%s) into a shell command" % (param, name)
++ warnings.warn(msg, UnsafeMailcapInput)
++ return None
++ res = res + param
+ # XXX To do:
+ # %n == number of parts if type is multipart/*
+ # %F == list of alternating type and filename for parts
+diff --git a/Lib/test/mailcap.txt b/Lib/test/mailcap.txt
+new file mode 100644
+index 00000000000..08a76e65941
+--- /dev/null
++++ b/Lib/test/mailcap.txt
+@@ -0,0 +1,39 @@
++# Mailcap file for test_mailcap; based on RFC 1524
++# Referred to by test_mailcap.py
++
++#
++# This is a comment.
++#
++
++application/frame; showframe %s; print="cat %s | lp"
++application/postscript; ps-to-terminal %s;\
++ needsterminal
++application/postscript; ps-to-terminal %s; \
++ compose=idraw %s
++application/x-dvi; xdvi %s
++application/x-movie; movieplayer %s; compose=moviemaker %s; \
++ description="Movie"; \
++ x11-bitmap="/usr/lib/Zmail/bitmaps/movie.xbm"
++application/*; echo "This is \"%t\" but \
++ is 50 \% Greek to me" \; cat %s; copiousoutput
++
++audio/basic; showaudio %s; compose=audiocompose %s; edit=audiocompose %s;\
++description="An audio fragment"
++audio/* ; /usr/local/bin/showaudio %t
++
++image/rgb; display %s
++#image/gif; display %s
++image/x-xwindowdump; display %s
++
++# The continuation char shouldn't \
++# make a difference in a comment.
++
++message/external-body; showexternal %s %{access-type} %{name} %{site} \
++ %{directory} %{mode} %{server}; needsterminal; composetyped = extcompose %s; \
++ description="A reference to data stored in an external location"
++
++text/richtext; shownonascii iso-8859-8 -e richtext -p %s; test=test "`echo \
++ %{charset} | tr '[A-Z]' '[a-z]'`" = iso-8859-8; copiousoutput
++
++video/*; animate %s
++video/mpeg; mpeg_play %s
+\ No newline at end of file
+diff --git a/Lib/test/test_mailcap.py b/Lib/test/test_mailcap.py
+new file mode 100644
+index 00000000000..35da7fb0741
+--- /dev/null
++++ b/Lib/test/test_mailcap.py
+@@ -0,0 +1,259 @@
++import copy
++import os
++import sys
++import test.support
++import unittest
++from test import support as os_helper
++from test import support as warnings_helper
++from collections import OrderedDict
++
++import mailcap
++
++
++# Location of mailcap file
++MAILCAPFILE = test.support.findfile("mailcap.txt")
++
++# Dict to act as mock mailcap entry for this test
++# The keys and values should match the contents of MAILCAPFILE
++
++MAILCAPDICT = {
++ 'application/x-movie':
++ [{'compose': 'moviemaker %s',
++ 'x11-bitmap': '"/usr/lib/Zmail/bitmaps/movie.xbm"',
++ 'description': '"Movie"',
++ 'view': 'movieplayer %s',
++ 'lineno': 4}],
++ 'application/*':
++ [{'copiousoutput': '',
++ 'view': 'echo "This is \\"%t\\" but is 50 \\% Greek to me" \\; cat %s',
++ 'lineno': 5}],
++ 'audio/basic':
++ [{'edit': 'audiocompose %s',
++ 'compose': 'audiocompose %s',
++ 'description': '"An audio fragment"',
++ 'view': 'showaudio %s',
++ 'lineno': 6}],
++ 'video/mpeg':
++ [{'view': 'mpeg_play %s', 'lineno': 13}],
++ 'application/postscript':
++ [{'needsterminal': '', 'view': 'ps-to-terminal %s', 'lineno': 1},
++ {'compose': 'idraw %s', 'view': 'ps-to-terminal %s', 'lineno': 2}],
++ 'application/x-dvi':
++ [{'view': 'xdvi %s', 'lineno': 3}],
++ 'message/external-body':
++ [{'composetyped': 'extcompose %s',
++ 'description': '"A reference to data stored in an external location"',
++ 'needsterminal': '',
++ 'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}',
++ 'lineno': 10}],
++ 'text/richtext':
++ [{'test': 'test "`echo %{charset} | tr \'[A-Z]\' \'[a-z]\'`" = iso-8859-8',
++ 'copiousoutput': '',
++ 'view': 'shownonascii iso-8859-8 -e richtext -p %s',
++ 'lineno': 11}],
++ 'image/x-xwindowdump':
++ [{'view': 'display %s', 'lineno': 9}],
++ 'audio/*':
++ [{'view': '/usr/local/bin/showaudio %t', 'lineno': 7}],
++ 'video/*':
++ [{'view': 'animate %s', 'lineno': 12}],
++ 'application/frame':
++ [{'print': '"cat %s | lp"', 'view': 'showframe %s', 'lineno': 0}],
++ 'image/rgb':
++ [{'view': 'display %s', 'lineno': 8}]
++}
++
++# In Python 2, mailcap doesn't return line numbers.
++# This test suite is copied from Python 3.11; for easier backporting we keep
++# data from there and remove the lineno.
++# So, for Python 2, MAILCAPDICT_DEPRECATED is the same as MAILCAPDICT
++MAILCAPDICT_DEPRECATED = MAILCAPDICT
++for entry_list in MAILCAPDICT_DEPRECATED.values():
++ for entry in entry_list:
++ entry.pop('lineno')
++
++
++class HelperFunctionTest(unittest.TestCase):
++
++ def test_listmailcapfiles(self):
++ # The return value for listmailcapfiles() will vary by system.
++ # So verify that listmailcapfiles() returns a list of strings that is of
++ # non-zero length.
++ mcfiles = mailcap.listmailcapfiles()
++ self.assertIsInstance(mcfiles, list)
++ for m in mcfiles:
++ self.assertIsInstance(m, str)
++ with os_helper.EnvironmentVarGuard() as env:
++ # According to RFC 1524, if MAILCAPS env variable exists, use that
++ # and only that.
++ if "MAILCAPS" in env:
++ env_mailcaps = env["MAILCAPS"].split(os.pathsep)
++ else:
++ env_mailcaps = ["/testdir1/.mailcap", "/testdir2/mailcap"]
++ env["MAILCAPS"] = os.pathsep.join(env_mailcaps)
++ mcfiles = mailcap.listmailcapfiles()
++ self.assertEqual(env_mailcaps, mcfiles)
++
++ def test_readmailcapfile(self):
++ # Test readmailcapfile() using test file. It should match MAILCAPDICT.
++ with open(MAILCAPFILE, 'r') as mcf:
++ d = mailcap.readmailcapfile(mcf)
++ self.assertDictEqual(d, MAILCAPDICT_DEPRECATED)
++
++ def test_lookup(self):
++ # Test without key
++
++ # In Python 2, 'video/mpeg' is tried before 'video/*'
++ # (unfixed bug: https://github.com/python/cpython/issues/59182 )
++ # So, these are in reverse order:
++ expected = [{'view': 'mpeg_play %s', },
++ {'view': 'animate %s', }]
++ actual = mailcap.lookup(MAILCAPDICT, 'video/mpeg')
++ self.assertListEqual(expected, actual)
++
++ # Test with key
++ key = 'compose'
++ expected = [{'edit': 'audiocompose %s',
++ 'compose': 'audiocompose %s',
++ 'description': '"An audio fragment"',
++ 'view': 'showaudio %s',
++ }]
++ actual = mailcap.lookup(MAILCAPDICT, 'audio/basic', key)
++ self.assertListEqual(expected, actual)
++
++ # Test on user-defined dicts without line numbers
++ expected = [{'view': 'mpeg_play %s'}, {'view': 'animate %s'}]
++ actual = mailcap.lookup(MAILCAPDICT_DEPRECATED, 'video/mpeg')
++ self.assertListEqual(expected, actual)
++
++ def test_subst(self):
++ plist = ['id=1', 'number=2', 'total=3']
++ # test case: ([field, MIMEtype, filename, plist=[]], <expected string>)
++ test_cases = [
++ (["", "audio/*", "foo.txt"], ""),
++ (["echo foo", "audio/*", "foo.txt"], "echo foo"),
++ (["echo %s", "audio/*", "foo.txt"], "echo foo.txt"),
++ (["echo %t", "audio/*", "foo.txt"], None),
++ (["echo %t", "audio/wav", "foo.txt"], "echo audio/wav"),
++ (["echo \\%t", "audio/*", "foo.txt"], "echo %t"),
++ (["echo foo", "audio/*", "foo.txt", plist], "echo foo"),
++ (["echo %{total}", "audio/*", "foo.txt", plist], "echo 3")
++ ]
++ for tc in test_cases:
++ self.assertEqual(mailcap.subst(*tc[0]), tc[1])
++
++class GetcapsTest(unittest.TestCase):
++
++ def test_mock_getcaps(self):
++ # Test mailcap.getcaps() using mock mailcap file in this dir.
++ # Temporarily override any existing system mailcap file by pointing the
++ # MAILCAPS environment variable to our mock file.
++ with os_helper.EnvironmentVarGuard() as env:
++ env["MAILCAPS"] = MAILCAPFILE
++ caps = mailcap.getcaps()
++ self.assertDictEqual(caps, MAILCAPDICT)
++
++ def test_system_mailcap(self):
++ # Test mailcap.getcaps() with mailcap file(s) on system, if any.
++ caps = mailcap.getcaps()
++ self.assertIsInstance(caps, dict)
++ mailcapfiles = mailcap.listmailcapfiles()
++ existingmcfiles = [mcf for mcf in mailcapfiles if os.path.exists(mcf)]
++ if existingmcfiles:
++ # At least 1 mailcap file exists, so test that.
++ for (k, v) in caps.items():
++ self.assertIsInstance(k, str)
++ self.assertIsInstance(v, list)
++ for e in v:
++ self.assertIsInstance(e, dict)
++ else:
++ # No mailcap files on system. getcaps() should return empty dict.
++ self.assertEqual({}, caps)
++
++
++class FindmatchTest(unittest.TestCase):
++
++ def test_findmatch(self):
++
++ # default findmatch arguments
++ c = MAILCAPDICT
++ fname = "foo.txt"
++ plist = ["access-type=default", "name=john", "site=python.org",
++ "directory=/tmp", "mode=foo", "server=bar"]
++ audio_basic_entry = {
++ 'edit': 'audiocompose %s',
++ 'compose': 'audiocompose %s',
++ 'description': '"An audio fragment"',
++ 'view': 'showaudio %s',
++ }
++ audio_entry = {"view": "/usr/local/bin/showaudio %t", }
++ video_entry = {'view': 'animate %s', }
++ mpeg_entry = {'view': 'mpeg_play %s', }
++ message_entry = {
++ 'composetyped': 'extcompose %s',
++ 'description': '"A reference to data stored in an external location"', 'needsterminal': '',
++ 'view': 'showexternal %s %{access-type} %{name} %{site} %{directory} %{mode} %{server}',
++ }
++
++ # test case: (findmatch args, findmatch keyword args, expected output)
++ # positional args: caps, MIMEtype
++ # keyword args: key="view", filename="/dev/null", plist=[]
++ # output: (command line, mailcap entry)
++ cases = [
++ ([{}, "video/mpeg"], {}, (None, None)),
++ ([c, "foo/bar"], {}, (None, None)),
++
++ # In Python 2, 'video/mpeg' is tried before 'video/*'
++ # (unfixed bug: https://github.com/python/cpython/issues/59182 )
++ #([c, "video/mpeg"], {}, ('animate /dev/null', video_entry)),
++ ([c, "video/mpeg"], {}, ('mpeg_play /dev/null', mpeg_entry)),
++
++ ([c, "audio/basic", "edit"], {}, ("audiocompose /dev/null", audio_basic_entry)),
++ ([c, "audio/basic", "compose"], {}, ("audiocompose /dev/null", audio_basic_entry)),
++ ([c, "audio/basic", "description"], {}, ('"An audio fragment"', audio_basic_entry)),
++ ([c, "audio/basic", "foobar"], {}, (None, None)),
++ ([c, "video/*"], {"filename": fname}, ("animate %s" % fname, video_entry)),
++ ([c, "audio/basic", "compose"],
++ {"filename": fname},
++ ("audiocompose %s" % fname, audio_basic_entry)),
++ ([c, "audio/basic"],
++ {"key": "description", "filename": fname},
++ ('"An audio fragment"', audio_basic_entry)),
++ ([c, "audio/*"],
++ {"filename": fname},
++ (None, None)),
++ ([c, "audio/wav"],
++ {"filename": fname},
++ ("/usr/local/bin/showaudio audio/wav", audio_entry)),
++ ([c, "message/external-body"],
++ {"plist": plist},
++ ("showexternal /dev/null default john python.org /tmp foo bar", message_entry))
++ ]
++ self._run_cases(cases)
++
++ @unittest.skipUnless(os.name == "posix", "Requires 'test' command on system")
++ @unittest.skipIf(sys.platform == "vxworks", "'test' command is not supported on VxWorks")
++ def test_test(self):
++ # findmatch() will automatically check any "test" conditions and skip
++ # the entry if the check fails.
++ caps = {"test/pass": [{"test": "test 1 -eq 1"}],
++ "test/fail": [{"test": "test 1 -eq 0"}]}
++ # test case: (findmatch args, findmatch keyword args, expected output)
++ # positional args: caps, MIMEtype, key ("test")
++ # keyword args: N/A
++ # output: (command line, mailcap entry)
++ cases = [
++ # findmatch will return the mailcap entry for test/pass because it evaluates to true
++ ([caps, "test/pass", "test"], {}, ("test 1 -eq 1", {"test": "test 1 -eq 1"})),
++ # findmatch will return None because test/fail evaluates to false
++ ([caps, "test/fail", "test"], {}, (None, None))
++ ]
++ self._run_cases(cases)
++
++ def _run_cases(self, cases):
++ for c in cases:
++ self.assertEqual(mailcap.findmatch(*c[0], **c[1]), c[2])
++
++
++def test_main():
++ test.support.run_unittest(HelperFunctionTest, GetcapsTest, FindmatchTest)
+diff --git a/Misc/NEWS.d/next/Security/2022-04-27-18-25-30.gh-issue-68966.gjS8zs.rst b/Misc/NEWS.d/next/Security/2022-04-27-18-25-30.gh-issue-68966.gjS8zs.rst
+new file mode 100644
+index 00000000000..da81a1f6993
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2022-04-27-18-25-30.gh-issue-68966.gjS8zs.rst
+@@ -0,0 +1,4 @@
++The deprecated mailcap module now refuses to inject unsafe text (filenames,
++MIME types, parameters) into shell commands. Instead of using such text, it
++will warn and act as if a match was not found (or for test commands, as if
++the test failed).
diff --git a/CVE-2019-20907.patch b/CVE-2019-20907.patch
new file mode 100644
index 0000000..fa05cf0
--- /dev/null
+++ b/CVE-2019-20907.patch
@@ -0,0 +1,39 @@
+From 893e6e3aee483d262df70656a68f63f601720fcd Mon Sep 17 00:00:00 2001
+From: Rishi <rishi_devan at mail.com>
+Date: Wed, 15 Jul 2020 13:51:00 +0200
+Subject: [PATCH 01/36] bpo-39017: Avoid infinite loop in the tarfile module
+ (GH-21454)
+
+Avoid infinite loop when reading specially crafted TAR files using the tarfile module
+(CVE-2019-20907).
+
+[stripped test to avoid binary patch]
+---
+ Lib/tarfile.py | 2 ++
+ .../next/Library/2020-07-12-22-16-58.bpo-39017.x3Cg-9.rst | 1 +
+ 2 files changed, 3 insertions(+)
+ create mode 100644 Misc/NEWS.d/next/Library/2020-07-12-22-16-58.bpo-39017.x3Cg-9.rst
+
+diff --git a/Lib/tarfile.py b/Lib/tarfile.py
+index adf91d5382..574a6bb279 100644
+--- a/Lib/tarfile.py
++++ b/Lib/tarfile.py
+@@ -1400,6 +1400,8 @@ class TarInfo(object):
+
+ length, keyword = match.groups()
+ length = int(length)
++ if length == 0:
++ raise InvalidHeaderError("invalid header")
+ value = buf[match.end(2) + 1:match.start(1) + length - 1]
+
+ keyword = keyword.decode("utf8")
+diff --git a/Misc/NEWS.d/next/Library/2020-07-12-22-16-58.bpo-39017.x3Cg-9.rst b/Misc/NEWS.d/next/Library/2020-07-12-22-16-58.bpo-39017.x3Cg-9.rst
+new file mode 100644
+index 0000000000..ad26676f8b
+--- /dev/null
++++ b/Misc/NEWS.d/next/Library/2020-07-12-22-16-58.bpo-39017.x3Cg-9.rst
+@@ -0,0 +1 @@
++Avoid infinite loop when reading specially crafted TAR files using the tarfile module (CVE-2019-20907).
+--
+2.38.1
+
diff --git a/CVE-2020-26116.patch b/CVE-2020-26116.patch
new file mode 100644
index 0000000..8f57119
--- /dev/null
+++ b/CVE-2020-26116.patch
@@ -0,0 +1,93 @@
+From 138e2caeb4827ccfd1eaff2cf63afb79dfeeb3c4 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= <mgorny at gentoo.org>
+Date: Thu, 10 Sep 2020 13:39:48 +0200
+Subject: [PATCH 03/36] bpo-39603: Prevent header injection in http methods
+ (GH-18485) (GH-21539)
+
+reject control chars in http method in http.client.putrequest to prevent http header injection
+(cherry picked from commit 8ca8a2e8fb068863c1138f07e3098478ef8be12e)
+
+Co-authored-by: AMIR <31338382+amiremohamadi at users.noreply.github.com>
+
+[rebased for py2.7]
+---
+ Lib/httplib.py | 17 +++++++++++++++++
+ Lib/test/test_httplib.py | 20 ++++++++++++++++++++
+ 2 files changed, 37 insertions(+)
+
+diff --git a/Lib/httplib.py b/Lib/httplib.py
+index fcc4152aaf..81a08d5d71 100644
+--- a/Lib/httplib.py
++++ b/Lib/httplib.py
+@@ -257,6 +257,10 @@ _contains_disallowed_url_pchar_re = re.compile('[\x00-\x20\x7f-\xff]')
+ # _is_allowed_url_pchars_re = re.compile(r"^[/!$&'()*+,;=:@%a-zA-Z0-9._~-]+$")
+ # We are more lenient for assumed real world compatibility purposes.
+
++# These characters are not allowed within HTTP method names
++# to prevent http header injection.
++_contains_disallowed_method_pchar_re = re.compile('[\x00-\x1f]')
++
+ # We always set the Content-Length header for these methods because some
+ # servers will otherwise respond with a 411
+ _METHODS_EXPECTING_BODY = {'PATCH', 'POST', 'PUT'}
+@@ -935,6 +939,8 @@ class HTTPConnection:
+ else:
+ raise CannotSendRequest()
+
++ self._validate_method(method)
++
+ # Save the method for use later in the response phase
+ self._method = method
+
+@@ -1020,6 +1026,17 @@ class HTTPConnection:
+ # On Python 2, request is already encoded (default)
+ return request
+
++ def _validate_method(self, method):
++ """Validate a method name for putrequest."""
++ # prevent http header injection
++ match = _contains_disallowed_method_pchar_re.search(method)
++ if match:
++ msg = (
++ "method can't contain control characters. {method!r} "
++ "(found at least {matched!r})"
++ ).format(matched=match.group(), method=method)
++ raise ValueError(msg)
++
+ def _validate_path(self, url):
+ """Validate a url for putrequest."""
+ # Prevent CVE-2019-9740.
+diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py
+index d8a57f7353..e20a0986dc 100644
+--- a/Lib/test/test_httplib.py
++++ b/Lib/test/test_httplib.py
+@@ -384,6 +384,26 @@ class HeaderTests(TestCase):
+ with self.assertRaisesRegexp(ValueError, 'Invalid header'):
+ conn.putheader(name, value)
+
++ def test_invalid_method_names(self):
++ methods = (
++ 'GET\r',
++ 'POST\n',
++ 'PUT\n\r',
++ 'POST\nValue',
++ 'POST\nHOST:abc',
++ 'GET\nrHost:abc\n',
++ 'POST\rRemainder:\r',
++ 'GET\rHOST:\n',
++ '\nPUT'
++ )
++
++ for method in methods:
++ with self.assertRaisesRegexp(
++ ValueError, "method can't contain control characters"):
++ conn = httplib.HTTPConnection('example.com')
++ conn.sock = FakeSocket(None)
++ conn.request(method=method, url="/")
++
+
+ class BasicTest(TestCase):
+ def test_status_lines(self):
+--
+2.38.1
+
diff --git a/CVE-2020-27619.patch b/CVE-2020-27619.patch
new file mode 100644
index 0000000..c429672
--- /dev/null
+++ b/CVE-2020-27619.patch
@@ -0,0 +1,119 @@
+From 6a6c4240fa1e628dbcca09fdde39aea4d8eb6138 Mon Sep 17 00:00:00 2001
+From: "Miss Skeleton (bot)" <31488909+miss-islington at users.noreply.github.com>
+Date: Mon, 19 Oct 2020 21:46:10 -0700
+Subject: [PATCH 05/36] bpo-41944: No longer call eval() on content received
+ via HTTP in the CJK codec tests (GH-22566) (GH-22579)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+(cherry picked from commit 2ef5caa58febc8968e670e39e3d37cf8eef3cab8)
+
+Co-authored-by: Serhiy Storchaka <storchaka at gmail.com>
+
+Rebased for Python 2.7 by Michał Górny <mgorny at gentoo.org>
+---
+ Lib/test/multibytecodec_support.py | 23 +++++++------------
+ .../2020-10-05-17-43-46.bpo-41944.rf1dYb.rst | 1 +
+ 2 files changed, 9 insertions(+), 15 deletions(-)
+ create mode 100644 Misc/NEWS.d/next/Tests/2020-10-05-17-43-46.bpo-41944.rf1dYb.rst
+
+diff --git a/Lib/test/multibytecodec_support.py b/Lib/test/multibytecodec_support.py
+index 5b2329b6d8..b7d7a3aba7 100644
+--- a/Lib/test/multibytecodec_support.py
++++ b/Lib/test/multibytecodec_support.py
+@@ -279,30 +279,23 @@ class TestBase_Mapping(unittest.TestCase):
+ self._test_mapping_file_plain()
+
+ def _test_mapping_file_plain(self):
+- _unichr = lambda c: eval("u'\\U%08x'" % int(c, 16))
+- unichrs = lambda s: u''.join(_unichr(c) for c in s.split('+'))
++ def unichrs(s):
++ return ''.join(chr(int(x, 16)) for x in s.split('+'))
++
+ urt_wa = {}
+
+ with self.open_mapping_file() as f:
+ for line in f:
+ if not line:
+ break
+- data = line.split('#')[0].strip().split()
++ data = line.split('#')[0].split()
+ if len(data) != 2:
+ continue
+
+- csetval = eval(data[0])
+- if csetval <= 0x7F:
+- csetch = chr(csetval & 0xff)
+- elif csetval >= 0x1000000:
+- csetch = chr(csetval >> 24) + chr((csetval >> 16) & 0xff) + \
+- chr((csetval >> 8) & 0xff) + chr(csetval & 0xff)
+- elif csetval >= 0x10000:
+- csetch = chr(csetval >> 16) + \
+- chr((csetval >> 8) & 0xff) + chr(csetval & 0xff)
+- elif csetval >= 0x100:
+- csetch = chr(csetval >> 8) + chr(csetval & 0xff)
+- else:
++ if data[0][:2] != '0x':
++ self.fail("Invalid line: {line!r}".format(line=line))
++ csetch = bytes.fromhex(data[0][2:])
++ if len(csetch) == 1 and 0x80 <= csetch[0]:
+ continue
+
+ unich = unichrs(data[1])
+diff --git a/Misc/NEWS.d/next/Tests/2020-10-05-17-43-46.bpo-41944.rf1dYb.rst b/Misc/NEWS.d/next/Tests/2020-10-05-17-43-46.bpo-41944.rf1dYb.rst
+new file mode 100644
+index 0000000000..4f9782f1c8
+--- /dev/null
++++ b/Misc/NEWS.d/next/Tests/2020-10-05-17-43-46.bpo-41944.rf1dYb.rst
+@@ -0,0 +1 @@
++Tests for CJK codecs no longer call ``eval()`` on content received via HTTP.
+--
+2.38.1
+
+From ed1aa2f4738efe948242f252bcb0aa0b4314d2a2 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= <mgorny at gentoo.org>
+Date: Fri, 5 Mar 2021 10:34:50 +0100
+Subject: [PATCH 09/36] py2-ize the CJK codec test
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Signed-off-by: Michał Górny <mgorny at gentoo.org>
+---
+ Lib/test/multibytecodec_support.py | 5 +++--
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+diff --git a/Lib/test/multibytecodec_support.py b/Lib/test/multibytecodec_support.py
+index b7d7a3aba7..661ef9ee37 100644
+--- a/Lib/test/multibytecodec_support.py
++++ b/Lib/test/multibytecodec_support.py
+@@ -2,6 +2,7 @@
+ # Common Unittest Routines for CJK codecs
+ #
+
++import binascii
+ import codecs
+ import os
+ import re
+@@ -280,7 +281,7 @@ class TestBase_Mapping(unittest.TestCase):
+
+ def _test_mapping_file_plain(self):
+ def unichrs(s):
+- return ''.join(chr(int(x, 16)) for x in s.split('+'))
++ return ''.join(unichr(int(x, 16)) for x in s.split('+'))
+
+ urt_wa = {}
+
+@@ -294,7 +295,7 @@ class TestBase_Mapping(unittest.TestCase):
+
+ if data[0][:2] != '0x':
+ self.fail("Invalid line: {line!r}".format(line=line))
+- csetch = bytes.fromhex(data[0][2:])
++ csetch = binascii.a2b_hex(data[0][2:])
+ if len(csetch) == 1 and 0x80 <= csetch[0]:
+ continue
+
+--
+2.38.1
+
diff --git a/CVE-2020-8492.patch b/CVE-2020-8492.patch
new file mode 100644
index 0000000..de21686
--- /dev/null
+++ b/CVE-2020-8492.patch
@@ -0,0 +1,213 @@
+From 2273e65e11dd0234f2f51ebaef61fc6e848d4059 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= <mgorny at gentoo.org>
+Date: Thu, 10 Sep 2020 13:35:39 +0200
+Subject: [PATCH 02/36] bpo-39503: CVE-2020-8492: Fix AbstractBasicAuthHandler
+ (GH-18284) (GH-19304)
+
+The AbstractBasicAuthHandler class of the urllib.request module uses
+an inefficient regular expression which can be exploited by an
+attacker to cause a denial of service. Fix the regex to prevent the
+catastrophic backtracking. Vulnerability reported by Ben Caller
+and Matt Schwager.
+
+AbstractBasicAuthHandler of urllib.request now parses all
+WWW-Authenticate HTTP headers and accepts multiple challenges per
+header: use the realm of the first Basic challenge.
+
+Co-Authored-By: Serhiy Storchaka <storchaka at gmail.com>
+(cherry picked from commit 0b297d4ff1c0e4480ad33acae793fbaf4bf015b4)
+
+[rebased for py2.7]
+---
+ Lib/test/test_urllib2.py | 81 ++++++++++++++++++++++++++--------------
+ Lib/urllib2.py | 60 +++++++++++++++++++++++------
+ 2 files changed, 101 insertions(+), 40 deletions(-)
+
+diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py
+index 20a0f58143..0adbb13c43 100644
+--- a/Lib/test/test_urllib2.py
++++ b/Lib/test/test_urllib2.py
+@@ -1128,42 +1128,67 @@ class HandlerTests(unittest.TestCase):
+ self.assertEqual(req.get_host(), "proxy.example.com:3128")
+ self.assertEqual(req.get_header("Proxy-authorization"),"FooBar")
+
+- def test_basic_auth(self, quote_char='"'):
++ def check_basic_auth(self, headers, realm):
+ opener = OpenerDirector()
+ password_manager = MockPasswordManager()
+ auth_handler = urllib2.HTTPBasicAuthHandler(password_manager)
+- realm = "ACME Widget Store"
+- http_handler = MockHTTPHandler(
+- 401, 'WWW-Authenticate: Basic realm=%s%s%s\r\n\r\n' %
+- (quote_char, realm, quote_char) )
++ body = '\r\n'.join(headers) + '\r\n\r\n'
++ http_handler = MockHTTPHandler(401, body)
+ opener.add_handler(auth_handler)
+ opener.add_handler(http_handler)
+ self._test_basic_auth(opener, auth_handler, "Authorization",
+ realm, http_handler, password_manager,
+ "http://acme.example.com/protected",
+- "http://acme.example.com/protected"
+- )
+-
+- def test_basic_auth_with_single_quoted_realm(self):
+- self.test_basic_auth(quote_char="'")
+-
+- def test_basic_auth_with_unquoted_realm(self):
+- opener = OpenerDirector()
+- password_manager = MockPasswordManager()
+- auth_handler = urllib2.HTTPBasicAuthHandler(password_manager)
+- realm = "ACME Widget Store"
+- http_handler = MockHTTPHandler(
+- 401, 'WWW-Authenticate: Basic realm=%s\r\n\r\n' % realm)
+- opener.add_handler(auth_handler)
+- opener.add_handler(http_handler)
+- msg = "Basic Auth Realm was unquoted"
+- with test_support.check_warnings((msg, UserWarning)):
+- self._test_basic_auth(opener, auth_handler, "Authorization",
+- realm, http_handler, password_manager,
+- "http://acme.example.com/protected",
+- "http://acme.example.com/protected"
+- )
+-
++ "http://acme.example.com/protected")
++
++ def test_basic_auth(self):
++ realm = "realm2 at example.com"
++ realm2 = "realm2 at example.com"
++ basic = 'Basic realm="{realm}"'.format(realm=realm)
++ basic2 = 'Basic realm="{realm2}"'.format(realm2=realm2)
++ other_no_realm = 'Otherscheme xxx'
++ digest = ('Digest realm="{realm2}", '
++ 'qop="auth, auth-int", '
++ 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", '
++ 'opaque="5ccc069c403ebaf9f0171e9517f40e41"'
++ .format(realm2=realm2))
++ for realm_str in (
++ # test "quote" and 'quote'
++ 'Basic realm="{realm}"'.format(realm=realm),
++ "Basic realm='{realm}'".format(realm=realm),
++
++ # charset is ignored
++ 'Basic realm="{realm}", charset="UTF-8"'.format(realm=realm),
++
++ # Multiple challenges per header
++ ', '.join((basic, basic2)),
++ ', '.join((basic, other_no_realm)),
++ ', '.join((other_no_realm, basic)),
++ ', '.join((basic, digest)),
++ ', '.join((digest, basic)),
++ ):
++ headers = ['WWW-Authenticate: {realm_str}'
++ .format(realm_str=realm_str)]
++ self.check_basic_auth(headers, realm)
++
++ # no quote: expect a warning
++ with test_support.check_warnings(("Basic Auth Realm was unquoted",
++ UserWarning)):
++ headers = ['WWW-Authenticate: Basic realm={realm}'
++ .format(realm=realm)]
++ self.check_basic_auth(headers, realm)
++
++ # Multiple headers: one challenge per header.
++ # Use the first Basic realm.
++ for challenges in (
++ [basic, basic2],
++ [basic, digest],
++ [digest, basic],
++ ):
++ headers = ['WWW-Authenticate: {challenge}'
++ .format(challenge=challenge)
++ for challenge in challenges]
++ self.check_basic_auth(headers, realm)
+
+ def test_proxy_basic_auth(self):
+ opener = OpenerDirector()
+diff --git a/Lib/urllib2.py b/Lib/urllib2.py
+index 8b634ada37..b2d1fad6f2 100644
+--- a/Lib/urllib2.py
++++ b/Lib/urllib2.py
+@@ -856,8 +856,15 @@ class AbstractBasicAuthHandler:
+
+ # allow for double- and single-quoted realm values
+ # (single quotes are a violation of the RFC, but appear in the wild)
+- rx = re.compile('(?:.*,)*[ \t]*([^ \t]+)[ \t]+'
+- 'realm=(["\']?)([^"\']*)\\2', re.I)
++ rx = re.compile('(?:^|,)' # start of the string or ','
++ '[ \t]*' # optional whitespaces
++ '([^ \t]+)' # scheme like "Basic"
++ '[ \t]+' # mandatory whitespaces
++ # realm=xxx
++ # realm='xxx'
++ # realm="xxx"
++ 'realm=(["\']?)([^"\']*)\\2',
++ re.I)
+
+ # XXX could pre-emptively send auth info already accepted (RFC 2617,
+ # end of section 2, and section 1.2 immediately after "credentials"
+@@ -869,23 +876,52 @@ class AbstractBasicAuthHandler:
+ self.passwd = password_mgr
+ self.add_password = self.passwd.add_password
+
++ def _parse_realm(self, header):
++ # parse WWW-Authenticate header: accept multiple challenges per header
++ found_challenge = False
++ for mo in AbstractBasicAuthHandler.rx.finditer(header):
++ scheme, quote, realm = mo.groups()
++ if quote not in ['"', "'"]:
++ warnings.warn("Basic Auth Realm was unquoted",
++ UserWarning, 3)
++
++ yield (scheme, realm)
++
++ found_challenge = True
++
++ if not found_challenge:
++ if header:
++ scheme = header.split()[0]
++ else:
++ scheme = ''
++ yield (scheme, None)
+
+ def http_error_auth_reqed(self, authreq, host, req, headers):
+ # host may be an authority (without userinfo) or a URL with an
+ # authority
+- # XXX could be multiple headers
+- authreq = headers.get(authreq, None)
++ headers = headers.getheaders(authreq)
++ if not headers:
++ # no header found
++ return
+
+- if authreq:
+- mo = AbstractBasicAuthHandler.rx.search(authreq)
+- if mo:
+- scheme, quote, realm = mo.groups()
+- if quote not in ['"', "'"]:
+- warnings.warn("Basic Auth Realm was unquoted",
+- UserWarning, 2)
+- if scheme.lower() == 'basic':
++ unsupported = None
++ for header in headers:
++ for scheme, realm in self._parse_realm(header):
++ if scheme.lower() != 'basic':
++ unsupported = scheme
++ continue
++
++ if realm is not None:
++ # Use the first matching Basic challenge.
++ # Ignore following challenges even if they use the Basic
++ # scheme.
+ return self.retry_http_basic_auth(host, req, realm)
+
++ if unsupported is not None:
++ raise ValueError("AbstractBasicAuthHandler does not "
++ "support the following scheme: %r"
++ % (scheme,))
++
+ def retry_http_basic_auth(self, host, req, realm):
+ user, pw = self.passwd.find_user_password(realm, host)
+ if pw is not None:
+--
+2.38.1
+
diff --git a/CVE-2021-23336.patch b/CVE-2021-23336.patch
new file mode 100644
index 0000000..f47be77
--- /dev/null
+++ b/CVE-2021-23336.patch
@@ -0,0 +1,390 @@
+From 255d1d464a7f45a20986aa014c78e85ef47e6591 Mon Sep 17 00:00:00 2001
+From: Senthil Kumaran <senthil at uthcode.com>
+Date: Mon, 15 Feb 2021 11:16:43 -0800
+Subject: [PATCH 08/36] [3.6] bpo-42967: only use '&' as a query string
+ separator (GH-24297) (GH-24532)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+bpo-42967: [security] Address a web cache-poisoning issue reported in
+urllib.parse.parse_qsl().
+
+urllib.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.
+
+Co-authored-by: Éric Araujo <merwok at netwok.org>
+Co-authored-by: Ken Jin <28750310+Fidget-Spinner at users.noreply.github.com>
+Co-authored-by: Adam Goldschmidt <adamgold7 at gmail.com>
+
+Rebased for Python 2.7 by Michał Górny
+---
+ Doc/library/cgi.rst | 7 +++-
+ Doc/library/urlparse.rst | 23 ++++++++++-
+ Lib/cgi.py | 20 +++++++---
+ Lib/test/test_cgi.py | 29 +++++++++++---
+ Lib/test/test_urlparse.py | 38 +++++++++----------
+ Lib/urlparse.py | 22 ++++++++---
+ .../2021-02-14-15-59-16.bpo-42967.YApqDS.rst | 1 +
+ 7 files changed, 100 insertions(+), 40 deletions(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2021-02-14-15-59-16.bpo-42967.YApqDS.rst
+
+diff --git a/Doc/library/cgi.rst b/Doc/library/cgi.rst
+index ecd62c8c01..b85cdd8b61 100644
+--- a/Doc/library/cgi.rst
++++ b/Doc/library/cgi.rst
+@@ -285,10 +285,10 @@ These are useful if you want more control, or if you want to employ some of the
+ algorithms implemented in this module in other circumstances.
+
+
+-.. function:: parse(fp[, environ[, keep_blank_values[, strict_parsing]]])
++.. function:: parse(fp[, environ[, keep_blank_values[, strict_parsing]]], separator="&")
+
+ Parse a query in the environment or from a file (the file defaults to
+- ``sys.stdin`` and environment defaults to ``os.environ``). The *keep_blank_values* and *strict_parsing* parameters are
++ ``sys.stdin`` and environment defaults to ``os.environ``). The *keep_blank_values*, *strict_parsing* and *separator* parameters are
+ passed to :func:`urlparse.parse_qs` unchanged.
+
+
+@@ -316,6 +316,9 @@ algorithms implemented in this module in other circumstances.
+ Note that this does not parse nested multipart parts --- use
+ :class:`FieldStorage` for that.
+
++ .. versionchanged:: 3.6.13
++ Added the *separator* parameter.
++
+
+ .. function:: parse_header(string)
+
+diff --git a/Doc/library/urlparse.rst b/Doc/library/urlparse.rst
+index 0989c88c30..2f8e4c5a44 100644
+--- a/Doc/library/urlparse.rst
++++ b/Doc/library/urlparse.rst
+@@ -136,7 +136,7 @@ The :mod:`urlparse` module defines the following functions:
+ now raise :exc:`ValueError`.
+
+
+-.. function:: parse_qs(qs[, keep_blank_values[, strict_parsing[, max_num_fields]]])
++.. function:: parse_qs(qs[, keep_blank_values[, strict_parsing[, max_num_fields]]], separator='&')
+
+ Parse a query string given as a string argument (data of type
+ :mimetype:`application/x-www-form-urlencoded`). Data are returned as a
+@@ -157,6 +157,9 @@ The :mod:`urlparse` module defines the following functions:
+ read. If set, then throws a :exc:`ValueError` if there are more than
+ *max_num_fields* fields read.
+
++ The optional argument *separator* is the symbol to use for separating the
++ query arguments. It defaults to ``&``.
++
+ Use the :func:`urllib.urlencode` function to convert such dictionaries into
+ query strings.
+
+@@ -166,7 +169,14 @@ The :mod:`urlparse` module defines the following functions:
+ .. versionchanged:: 2.7.16
+ Added *max_num_fields* parameter.
+
+-.. function:: parse_qsl(qs[, keep_blank_values[, strict_parsing[, max_num_fields]]])
++ .. versionchanged:: 2.7.18-gentoo
++ Added *separator* parameter with the default value of ``&``. Earlier
++ Python versions allowed using both ``;`` and ``&`` as query parameter
++ separator. This has been changed to allow only a single separator key,
++ with ``&`` as the default separator.
++
++
++.. function:: parse_qsl(qs[, keep_blank_values[, strict_parsing[, max_num_fields]]], separator='&')
+
+ Parse a query string given as a string argument (data of type
+ :mimetype:`application/x-www-form-urlencoded`). Data are returned as a list of
+@@ -186,6 +196,9 @@ The :mod:`urlparse` module defines the following functions:
+ read. If set, then throws a :exc:`ValueError` if there are more than
+ *max_num_fields* fields read.
+
++ The optional argument *separator* is the symbol to use for separating the
++ query arguments. It defaults to ``&``.
++
+ Use the :func:`urllib.urlencode` function to convert such lists of pairs into
+ query strings.
+
+@@ -195,6 +208,12 @@ The :mod:`urlparse` module defines the following functions:
+ .. versionchanged:: 2.7.16
+ Added *max_num_fields* parameter.
+
++ .. versionchanged:: 2.7.18-gentoo
++ Added *separator* parameter with the default value of ``&``. Earlier
++ Python versions allowed using both ``;`` and ``&`` as query parameter
++ separator. This has been changed to allow only a single separator key,
++ with ``&`` as the default separator.
++
+ .. function:: urlunparse(parts)
+
+ Construct a URL from a tuple as returned by ``urlparse()``. The *parts* argument
+diff --git a/Lib/cgi.py b/Lib/cgi.py
+index 5b903e0347..9d0848b6b1 100755
+--- a/Lib/cgi.py
++++ b/Lib/cgi.py
+@@ -121,7 +121,8 @@ log = initlog # The current logging function
+ # 0 ==> unlimited input
+ maxlen = 0
+
+-def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
++def parse(fp=None, environ=os.environ, keep_blank_values=0,
++ strict_parsing=0, separator='&'):
+ """Parse a query in the environment or from a file (default stdin)
+
+ Arguments, all optional:
+@@ -140,6 +141,9 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
+ strict_parsing: flag indicating what to do with parsing errors.
+ If false (the default), errors are silently ignored.
+ If true, errors raise a ValueError exception.
++
++ separator: str. The symbol to use for separating the query arguments.
++ Defaults to &.
+ """
+ if fp is None:
+ fp = sys.stdin
+@@ -171,7 +175,8 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
+ else:
+ qs = ""
+ environ['QUERY_STRING'] = qs # XXX Shouldn't, really
+- return urlparse.parse_qs(qs, keep_blank_values, strict_parsing)
++ return urlparse.parse_qs(qs, keep_blank_values, strict_parsing,
++ separator=separator)
+
+
+ # parse query string function called from urlparse,
+@@ -395,7 +400,7 @@ class FieldStorage:
+
+ def __init__(self, fp=None, headers=None, outerboundary="",
+ environ=os.environ, keep_blank_values=0, strict_parsing=0,
+- max_num_fields=None):
++ max_num_fields=None, separator='&'):
+ """Constructor. Read multipart/* until last part.
+
+ Arguments, all optional:
+@@ -430,6 +435,7 @@ class FieldStorage:
+ self.keep_blank_values = keep_blank_values
+ self.strict_parsing = strict_parsing
+ self.max_num_fields = max_num_fields
++ self.separator = separator
+ if 'REQUEST_METHOD' in environ:
+ method = environ['REQUEST_METHOD'].upper()
+ self.qs_on_post = None
+@@ -613,7 +619,8 @@ class FieldStorage:
+ if self.qs_on_post:
+ qs += '&' + self.qs_on_post
+ query = urlparse.parse_qsl(qs, self.keep_blank_values,
+- self.strict_parsing, self.max_num_fields)
++ self.strict_parsing, self.max_num_fields,
++ separator=self.separator)
+ self.list = [MiniFieldStorage(key, value) for key, value in query]
+ self.skip_lines()
+
+@@ -629,7 +636,8 @@ class FieldStorage:
+ query = urlparse.parse_qsl(self.qs_on_post,
+ self.keep_blank_values,
+ self.strict_parsing,
+- self.max_num_fields)
++ self.max_num_fields,
++ separator=self.separator)
+ self.list.extend(MiniFieldStorage(key, value)
+ for key, value in query)
+ FieldStorageClass = None
+@@ -649,7 +657,7 @@ class FieldStorage:
+ headers = rfc822.Message(self.fp)
+ part = klass(self.fp, headers, ib,
+ environ, keep_blank_values, strict_parsing,
+- max_num_fields)
++ max_num_fields, separator=self.separator)
+
+ if max_num_fields is not None:
+ max_num_fields -= 1
+diff --git a/Lib/test/test_cgi.py b/Lib/test/test_cgi.py
+index 743c2afbd4..f414faa23b 100644
+--- a/Lib/test/test_cgi.py
++++ b/Lib/test/test_cgi.py
+@@ -61,12 +61,9 @@ parse_strict_test_cases = [
+ ("", ValueError("bad query field: ''")),
+ ("&", ValueError("bad query field: ''")),
+ ("&&", ValueError("bad query field: ''")),
+- (";", ValueError("bad query field: ''")),
+- (";&;", ValueError("bad query field: ''")),
+ # Should the next few really be valid?
+ ("=", {}),
+ ("=&=", {}),
+- ("=;=", {}),
+ # This rest seem to make sense
+ ("=a", {'': ['a']}),
+ ("&=a", ValueError("bad query field: ''")),
+@@ -81,8 +78,6 @@ parse_strict_test_cases = [
+ ("a=a+b&b=b+c", {'a': ['a b'], 'b': ['b c']}),
+ ("a=a+b&a=b+a", {'a': ['a b', 'b a']}),
+ ("x=1&y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
+- ("x=1;y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
+- ("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
+ ("Hbc5161168c542333633315dee1182227:key_store_seqid=400006&cuyer=r&view=bustomer&order_id=0bb2e248638833d48cb7fed300000f1b&expire=964546263&lobale=en-US&kid=130003.300038&ss=env",
+ {'Hbc5161168c542333633315dee1182227:key_store_seqid': ['400006'],
+ 'cuyer': ['r'],
+@@ -188,6 +183,30 @@ class CgiTests(unittest.TestCase):
+ self.assertEqual(expect[k], v)
+ self.assertItemsEqual(expect.values(), d.values())
+
++ def test_separator(self):
++ parse_semicolon = [
++ ("x=1;y=2.0", {'x': ['1'], 'y': ['2.0']}),
++ ("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
++ (";", ValueError("bad query field: ''")),
++ (";;", ValueError("bad query field: ''")),
++ ("=;a", ValueError("bad query field: 'a'")),
++ (";b=a", ValueError("bad query field: ''")),
++ ("b;=a", ValueError("bad query field: 'b'")),
++ ("a=a+b;b=b+c", {'a': ['a b'], 'b': ['b c']}),
++ ("a=a+b;a=b+a", {'a': ['a b', 'b a']}),
++ ]
++ for orig, expect in parse_semicolon:
++ env = {'QUERY_STRING': orig}
++ fs = cgi.FieldStorage(separator=';', environ=env)
++ if isinstance(expect, dict):
++ for key in expect.keys():
++ expect_val = expect[key]
++ self.assertIn(key, fs)
++ if len(expect_val) > 1:
++ self.assertEqual(fs.getvalue(key), expect_val)
++ else:
++ self.assertEqual(fs.getvalue(key), expect_val[0])
++
+ def test_log(self):
+ cgi.log("Testing")
+
+diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py
+index 86c4a0595c..0b2107339a 100644
+--- a/Lib/test/test_urlparse.py
++++ b/Lib/test/test_urlparse.py
+@@ -24,16 +24,20 @@ parse_qsl_test_cases = [
+ ("&a=b", [('a', 'b')]),
+ ("a=a+b&b=b+c", [('a', 'a b'), ('b', 'b c')]),
+ ("a=1&a=2", [('a', '1'), ('a', '2')]),
+- (";", []),
+- (";;", []),
+- (";a=b", [('a', 'b')]),
+- ("a=a+b;b=b+c", [('a', 'a b'), ('b', 'b c')]),
+- ("a=1;a=2", [('a', '1'), ('a', '2')]),
+- (b";", []),
+- (b";;", []),
+- (b";a=b", [(b'a', b'b')]),
+- (b"a=a+b;b=b+c", [(b'a', b'a b'), (b'b', b'b c')]),
+- (b"a=1;a=2", [(b'a', b'1'), (b'a', b'2')]),
++ (b"", []),
++ (b"&", []),
++ (b"&&", []),
++ (b"=", [(b'', b'')]),
++ (b"=a", [(b'', b'a')]),
++ (b"a", [(b'a', b'')]),
++ (b"a=", [(b'a', b'')]),
++ (b"&a=b", [(b'a', b'b')]),
++ (b"a=a+b&b=b+c", [(b'a', b'a b'), (b'b', b'b c')]),
++ (b"a=1&a=2", [(b'a', b'1'), (b'a', b'2')]),
++ (";a=b", [(';a', 'b')]),
++ ("a=a+b;b=b+c", [('a', 'a b;b=b c')]),
++ (b";a=b", [(b';a', b'b')]),
++ (b"a=a+b;b=b+c", [(b'a', b'a b;b=b c')]),
+ ]
+
+ parse_qs_test_cases = [
+@@ -57,16 +61,10 @@ parse_qs_test_cases = [
+ (b"&a=b", {b'a': [b'b']}),
+ (b"a=a+b&b=b+c", {b'a': [b'a b'], b'b': [b'b c']}),
+ (b"a=1&a=2", {b'a': [b'1', b'2']}),
+- (";", {}),
+- (";;", {}),
+- (";a=b", {'a': ['b']}),
+- ("a=a+b;b=b+c", {'a': ['a b'], 'b': ['b c']}),
+- ("a=1;a=2", {'a': ['1', '2']}),
+- (b";", {}),
+- (b";;", {}),
+- (b";a=b", {b'a': [b'b']}),
+- (b"a=a+b;b=b+c", {b'a': [b'a b'], b'b': [b'b c']}),
+- (b"a=1;a=2", {b'a': [b'1', b'2']}),
++ (";a=b", {';a': ['b']}),
++ ("a=a+b;b=b+c", {'a': ['a b;b=b c']}),
++ (b";a=b", {b';a': [b'b']}),
++ (b"a=a+b;b=b+c", {b'a':[ b'a b;b=b c']}),
+ ]
+
+ class UrlParseTestCase(unittest.TestCase):
+diff --git a/Lib/urlparse.py b/Lib/urlparse.py
+index 798b467b60..6c32727fce 100644
+--- a/Lib/urlparse.py
++++ b/Lib/urlparse.py
+@@ -382,7 +382,8 @@ def unquote(s):
+ append(item)
+ return ''.join(res)
+
+-def parse_qs(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None):
++def parse_qs(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None,
++ separator='&'):
+ """Parse a query given as a string argument.
+
+ Arguments:
+@@ -402,17 +403,22 @@ def parse_qs(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None):
+
+ max_num_fields: int. If set, then throws a ValueError if there
+ are more than n fields read by parse_qsl().
++
++ separator: str. The symbol to use for separating the query arguments.
++ Defaults to &.
++
+ """
+ dict = {}
+ for name, value in parse_qsl(qs, keep_blank_values, strict_parsing,
+- max_num_fields):
++ max_num_fields, separator=separator):
+ if name in dict:
+ dict[name].append(value)
+ else:
+ dict[name] = [value]
+ return dict
+
+-def parse_qsl(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None):
++def parse_qsl(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None,
++ separator='&'):
+ """Parse a query given as a string argument.
+
+ Arguments:
+@@ -432,17 +438,23 @@ def parse_qsl(qs, keep_blank_values=0, strict_parsing=0, max_num_fields=None):
+ max_num_fields: int. If set, then throws a ValueError if there
+ are more than n fields read by parse_qsl().
+
++ separator: str. The symbol to use for separating the query arguments.
++ Defaults to &.
++
+ Returns a list, as G-d intended.
+ """
++ if not separator or (not isinstance(separator, (str, bytes))):
++ raise ValueError("Separator must be of type string or bytes.")
++
+ # If max_num_fields is defined then check that the number of fields
+ # is less than max_num_fields. This prevents a memory exhaustion DOS
+ # attack via post bodies with many fields.
+ if max_num_fields is not None:
+- num_fields = 1 + qs.count('&') + qs.count(';')
++ num_fields = 1 + qs.count(separator)
+ if max_num_fields < num_fields:
+ raise ValueError('Max number of fields exceeded')
+
+- pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
++ pairs = [s1 for s1 in qs.split(separator)]
+ r = []
+ for name_value in pairs:
+ if not name_value and not strict_parsing:
+diff --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
+new file mode 100644
+index 0000000000..f08489b414
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2021-02-14-15-59-16.bpo-42967.YApqDS.rst
+@@ -0,0 +1 @@
++Fix web cache poisoning vulnerability by defaulting the query args separator to ``&``, and allowing the user to choose a custom separator.
+--
+2.38.1
+
diff --git a/CVE-2021-3177.patch b/CVE-2021-3177.patch
new file mode 100644
index 0000000..adbd360
--- /dev/null
+++ b/CVE-2021-3177.patch
@@ -0,0 +1,181 @@
+From fab838b2ee7cfb9037c24f0f18dfe01aa379b3f7 Mon Sep 17 00:00:00 2001
+From: Benjamin Peterson <benjamin at python.org>
+Date: Mon, 18 Jan 2021 15:11:46 -0600
+Subject: [PATCH 07/36] [3.6] closes bpo-42938: Replace snprintf with Python
+ unicode formatting in ctypes param reprs. (GH-24250)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+(cherry picked from commit 916610ef90a0d0761f08747f7b0905541f0977c7)
+
+Co-authored-by: Benjamin Peterson <benjamin at python.org>
+Rebased for Python 2.7 by Michał Górny <mgorny at gentoo.org>
+---
+ Lib/ctypes/test/test_parameters.py | 43 ++++++++++++++++
+ .../2021-01-18-09-27-31.bpo-42938.4Zn4Mp.rst | 2 +
+ Modules/_ctypes/callproc.c | 49 +++++++++----------
+ 3 files changed, 69 insertions(+), 25 deletions(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2021-01-18-09-27-31.bpo-42938.4Zn4Mp.rst
+
+diff --git a/Lib/ctypes/test/test_parameters.py b/Lib/ctypes/test/test_parameters.py
+index 23c1b6e225..3456882ccb 100644
+--- a/Lib/ctypes/test/test_parameters.py
++++ b/Lib/ctypes/test/test_parameters.py
+@@ -206,6 +206,49 @@ class SimpleTypesTestCase(unittest.TestCase):
+ with self.assertRaises(ZeroDivisionError):
+ WorseStruct().__setstate__({}, b'foo')
+
++ def test_parameter_repr(self):
++ from ctypes import (
++ c_bool,
++ c_char,
++ c_wchar,
++ c_byte,
++ c_ubyte,
++ c_short,
++ c_ushort,
++ c_int,
++ c_uint,
++ c_long,
++ c_ulong,
++ c_longlong,
++ c_ulonglong,
++ c_float,
++ c_double,
++ c_longdouble,
++ c_char_p,
++ c_wchar_p,
++ c_void_p,
++ )
++ self.assertRegexpMatches(repr(c_bool.from_param(True)), r"^<cparam '\?' at 0x[A-Fa-f0-9]+>$")
++ self.assertEqual(repr(c_char.from_param('a')), "<cparam 'c' (a)>")
++ self.assertRegexpMatches(repr(c_wchar.from_param('a')), r"^<cparam 'u' at 0x[A-Fa-f0-9]+>$")
++ self.assertEqual(repr(c_byte.from_param(98)), "<cparam 'b' (98)>")
++ self.assertEqual(repr(c_ubyte.from_param(98)), "<cparam 'B' (98)>")
++ self.assertEqual(repr(c_short.from_param(511)), "<cparam 'h' (511)>")
++ self.assertEqual(repr(c_ushort.from_param(511)), "<cparam 'H' (511)>")
++ self.assertRegexpMatches(repr(c_int.from_param(20000)), r"^<cparam '[li]' \(20000\)>$")
++ self.assertRegexpMatches(repr(c_uint.from_param(20000)), r"^<cparam '[LI]' \(20000\)>$")
++ self.assertRegexpMatches(repr(c_long.from_param(20000)), r"^<cparam '[li]' \(20000\)>$")
++ self.assertRegexpMatches(repr(c_ulong.from_param(20000)), r"^<cparam '[LI]' \(20000\)>$")
++ self.assertRegexpMatches(repr(c_longlong.from_param(20000)), r"^<cparam '[liq]' \(20000\)>$")
++ self.assertRegexpMatches(repr(c_ulonglong.from_param(20000)), r"^<cparam '[LIQ]' \(20000\)>$")
++ self.assertEqual(repr(c_float.from_param(1.5)), "<cparam 'f' (1.5)>")
++ self.assertEqual(repr(c_double.from_param(1.5)), "<cparam 'd' (1.5)>")
++ self.assertEqual(repr(c_double.from_param(1e300)), "<cparam 'd' (1e+300)>")
++ self.assertRegexpMatches(repr(c_longdouble.from_param(1.5)), r"^<cparam ('d' \(1.5\)|'g' at 0x[A-Fa-f0-9]+)>$")
++ self.assertRegexpMatches(repr(c_char_p.from_param(b'hihi')), "^<cparam 'z' \(0x[A-Fa-f0-9]+\)>$")
++ self.assertRegexpMatches(repr(c_wchar_p.from_param('hihi')), "^<cparam 'Z' \(0x[A-Fa-f0-9]+\)>$")
++ self.assertRegexpMatches(repr(c_void_p.from_param(0x12)), r"^<cparam 'P' \(0x0*12\)>$")
++
+ ################################################################
+
+ if __name__ == '__main__':
+diff --git a/Misc/NEWS.d/next/Security/2021-01-18-09-27-31.bpo-42938.4Zn4Mp.rst b/Misc/NEWS.d/next/Security/2021-01-18-09-27-31.bpo-42938.4Zn4Mp.rst
+new file mode 100644
+index 0000000000..7df65a156f
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2021-01-18-09-27-31.bpo-42938.4Zn4Mp.rst
+@@ -0,0 +1,2 @@
++Avoid static buffers when computing the repr of :class:`ctypes.c_double` and
++:class:`ctypes.c_longdouble` values.
+diff --git a/Modules/_ctypes/callproc.c b/Modules/_ctypes/callproc.c
+index 066fefc0cc..421addf353 100644
+--- a/Modules/_ctypes/callproc.c
++++ b/Modules/_ctypes/callproc.c
+@@ -460,50 +460,51 @@ PyCArg_dealloc(PyCArgObject *self)
+ static PyObject *
+ PyCArg_repr(PyCArgObject *self)
+ {
+- char buffer[256];
+ switch(self->tag) {
+ case 'b':
+ case 'B':
+- sprintf(buffer, "<cparam '%c' (%d)>",
++ return PyString_FromFormat("<cparam '%c' (%d)>",
+ self->tag, self->value.b);
+- break;
+ case 'h':
+ case 'H':
+- sprintf(buffer, "<cparam '%c' (%d)>",
++ return PyString_FromFormat("<cparam '%c' (%d)>",
+ self->tag, self->value.h);
+- break;
+ case 'i':
+ case 'I':
+- sprintf(buffer, "<cparam '%c' (%d)>",
++ return PyString_FromFormat("<cparam '%c' (%d)>",
+ self->tag, self->value.i);
+- break;
+ case 'l':
+ case 'L':
+- sprintf(buffer, "<cparam '%c' (%ld)>",
++ return PyString_FromFormat("<cparam '%c' (%ld)>",
+ self->tag, self->value.l);
+- break;
+
+ #ifdef HAVE_LONG_LONG
+ case 'q':
+ case 'Q':
+- sprintf(buffer,
++ return PyString_FromFormat(
+ "<cparam '%c' (%" PY_FORMAT_LONG_LONG "d)>",
+ self->tag, self->value.q);
+- break;
+ #endif
+ case 'd':
+- sprintf(buffer, "<cparam '%c' (%f)>",
+- self->tag, self->value.d);
+- break;
+- case 'f':
+- sprintf(buffer, "<cparam '%c' (%f)>",
+- self->tag, self->value.f);
+- break;
+-
++ case 'f': {
++ PyObject *f = PyFloat_FromDouble((self->tag == 'f') ? self->value.f : self->value.d);
++ if (f == NULL) {
++ return NULL;
++ }
++ PyObject *r = PyObject_Repr(f);
++ if (r == NULL) {
++ Py_DECREF(f);
++ return NULL;
++ }
++ PyObject *result = PyString_FromFormat(
++ "<cparam '%c' (%s)>", self->tag, PyString_AsString(r));
++ Py_DECREF(r);
++ Py_DECREF(f);
++ return result;
++ }
+ case 'c':
+- sprintf(buffer, "<cparam '%c' (%c)>",
++ return PyString_FromFormat("<cparam '%c' (%c)>",
+ self->tag, self->value.c);
+- break;
+
+ /* Hm, are these 'z' and 'Z' codes useful at all?
+ Shouldn't they be replaced by the functionality of c_string
+@@ -512,16 +513,14 @@ PyCArg_repr(PyCArgObject *self)
+ case 'z':
+ case 'Z':
+ case 'P':
+- sprintf(buffer, "<cparam '%c' (%p)>",
++ return PyString_FromFormat("<cparam '%c' (%p)>",
+ self->tag, self->value.p);
+ break;
+
+ default:
+- sprintf(buffer, "<cparam '%c' at %p>",
++ return PyString_FromFormat("<cparam '%c' at %p>",
+ self->tag, self);
+- break;
+ }
+- return PyString_FromString(buffer);
+ }
+
+ static PyMemberDef PyCArgType_members[] = {
+--
+2.38.1
+
diff --git a/CVE-2021-3733.patch b/CVE-2021-3733.patch
new file mode 100644
index 0000000..98d15df
--- /dev/null
+++ b/CVE-2021-3733.patch
@@ -0,0 +1,39 @@
+From f9e5d7ae605bd9739b1cf3423f03daa909ee4a0c Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington at users.noreply.github.com>
+Date: Wed, 7 Apr 2021 04:45:05 -0700
+Subject: [PATCH 11/36] bpo-43075: Fix ReDoS in urllib AbstractBasicAuthHandler
+ (GH-24391)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Fix Regular Expression Denial of Service (ReDoS) vulnerability in
+urllib.request.AbstractBasicAuthHandler. The ReDoS-vulnerable regex
+has quadratic worst-case complexity and it allows cause a denial of
+service when identifying crafted invalid RFCs. This ReDoS issue is on
+the client side and needs remote attackers to control the HTTP server.
+(cherry picked from commit 7215d1ae25525c92b026166f9d5cac85fb1defe1)
+
+Co-authored-by: Yeting Li <liyt at ios.ac.cn>
+(backported to Python 2.7 by Michał Górny)
+---
+ Lib/urllib2.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/Lib/urllib2.py b/Lib/urllib2.py
+index b2d1fad6f2..c92fb6afb8 100644
+--- a/Lib/urllib2.py
++++ b/Lib/urllib2.py
+@@ -858,7 +858,7 @@ class AbstractBasicAuthHandler:
+ # (single quotes are a violation of the RFC, but appear in the wild)
+ rx = re.compile('(?:^|,)' # start of the string or ','
+ '[ \t]*' # optional whitespaces
+- '([^ \t]+)' # scheme like "Basic"
++ '([^ \t,]+)' # scheme like "Basic"
+ '[ \t]+' # mandatory whitespaces
+ # realm=xxx
+ # realm='xxx'
+--
+2.38.1
+
diff --git a/CVE-2021-3737.patch b/CVE-2021-3737.patch
new file mode 100644
index 0000000..64622f9
--- /dev/null
+++ b/CVE-2021-3737.patch
@@ -0,0 +1,83 @@
+From dae96acef25d8c95aff9673e29aa5ddee862e9b4 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= <mgorny at gentoo.org>
+Date: Sat, 19 Jun 2021 20:46:09 +0200
+Subject: [PATCH 13/36] Backport bpo-44022: Fix http client infinite line
+ reading (DoS) after a HTTP 100 Continue (GH-25916)
+
+Backport the fix from the following commit:
+
+ commit 47895e31b6f626bc6ce47d175fe9d43c1098909d
+ Author: Gen Xu <xgbarry at gmail.com>
+ Date: 2021-05-06 00:42:41 +0200
+
+ bpo-44022: Fix http client infinite line reading (DoS) after a HTTP 100 Continue (GH-25916)
+
+ Fixes http.client potential denial of service where it could get stuck reading lines from a malicious server after a 100 Continue response.
+
+ Co-authored-by: Gregory P. Smith <greg at krypto.org>
+
+Instead of reusing the header reading code, I have just added explicit
+counter to avoid having to refactor the old code.
+
+Plus the improved test from:
+
+ commit e60ab843cbb016fb6ff8b4f418641ac05a9b2fcc
+ Author: Gregory P. Smith <greg at krypto.org>
+ Date: 2021-06-03 05:43:38 +0200
+
+ bpo-44022: Improve the regression test. (GH-26503)
+
+ It wasn't actually detecting the regression due to the
+ assertion being too lenient.
+---
+ Lib/httplib.py | 5 ++++-
+ Lib/test/test_httplib.py | 13 +++++++++++++
+ 2 files changed, 17 insertions(+), 1 deletion(-)
+
+diff --git a/Lib/httplib.py b/Lib/httplib.py
+index 81a08d5d71..ebfa59ff93 100644
+--- a/Lib/httplib.py
++++ b/Lib/httplib.py
+@@ -453,11 +453,14 @@ class HTTPResponse:
+ if status != CONTINUE:
+ break
+ # skip the header from the 100 response
++ header_count = 0
+ while True:
+ skip = self.fp.readline(_MAXLINE + 1)
+ if len(skip) > _MAXLINE:
+ raise LineTooLong("header line")
+- skip = skip.strip()
++ header_count += 1
++ if header_count > _MAXHEADERS:
++ raise HTTPException("got more than %d headers" % _MAXHEADERS)
+ if not skip:
+ break
+ if self.debuglevel > 0:
+diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py
+index e20a0986dc..5f6b1d0b20 100644
+--- a/Lib/test/test_httplib.py
++++ b/Lib/test/test_httplib.py
+@@ -675,6 +675,19 @@ class BasicTest(TestCase):
+ resp = httplib.HTTPResponse(FakeSocket(body))
+ self.assertRaises(httplib.LineTooLong, resp.begin)
+
++ def test_overflowing_header_limit_after_100(self):
++ body = (
++ 'HTTP/1.1 100 OK\r\n'
++ 'r\n' * 32768
++ )
++ resp = httplib.HTTPResponse(FakeSocket(body))
++ with self.assertRaises(httplib.HTTPException) as cm:
++ resp.begin()
++ # We must assert more because other reasonable errors that we
++ # do not want can also be HTTPException derived.
++ self.assertIn('got more than ', str(cm.exception))
++ self.assertIn('headers', str(cm.exception))
++
+ def test_overflowing_chunked_line(self):
+ body = (
+ 'HTTP/1.1 200 OK\r\n'
+--
+2.38.1
+
diff --git a/CVE-2021-4189.patch b/CVE-2021-4189.patch
new file mode 100644
index 0000000..92933da
--- /dev/null
+++ b/CVE-2021-4189.patch
@@ -0,0 +1,133 @@
+From fe31479be5ea4e611bd485516e48c0ce46be987c Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington at users.noreply.github.com>
+Date: Tue, 16 Mar 2021 14:08:30 -0700
+Subject: [PATCH 10/36] [3.6] bpo-43285 Make ftplib not trust the PASV
+ response. (GH-24838) (GH-24881) (GH-24882)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+The IPv4 address value returned from the server in response to the PASV command
+should not be trusted. This prevents a malicious FTP server from using the
+response to probe IPv4 address and port combinations on the client network.
+
+Instead of using the returned address, we use the IP address we're
+already connected to. This is the strategy other ftp clients adopted,
+and matches the only strategy available for the modern IPv6 EPSV command
+where the server response must return a port number and nothing else.
+
+For the rare user who _wants_ this ugly behavior, set a `trust_server_pasv_ipv4_address`
+attribute on your `ftplib.FTP` instance to True..
+(cherry picked from commit 0ab152c6b5d95caa2dc1a30fa96e10258b5f188e)
+
+Co-authored-by: Gregory P. Smith <greg at krypto.org>
+(cherry picked from commit 664d1d16274b47eea6ec92572e1ebf3939a6fa0c)
+
+Rebased for Python 2.7 by Michał Górny <mgorny at gentoo.org>
+---
+ Lib/ftplib.py | 9 +++++-
+ Lib/test/test_ftplib.py | 29 +++++++++++++++++--
+ .../2021-03-13-03-48-14.bpo-43285.g-Hah3.rst | 8 +++++
+ 3 files changed, 43 insertions(+), 3 deletions(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2021-03-13-03-48-14.bpo-43285.g-Hah3.rst
+
+diff --git a/Lib/ftplib.py b/Lib/ftplib.py
+index 6644554792..0550f0ab9f 100644
+--- a/Lib/ftplib.py
++++ b/Lib/ftplib.py
+@@ -108,6 +108,8 @@ class FTP:
+ file = None
+ welcome = None
+ passiveserver = 1
++ # Disables https://bugs.python.org/issue43285 security if set to True.
++ trust_server_pasv_ipv4_address = False
+
+ # Initialization method (called by class instantiation).
+ # Initialize host to localhost, port to standard ftp port
+@@ -310,8 +312,13 @@ class FTP:
+ return sock
+
+ def makepasv(self):
++ """Internal: Does the PASV or EPSV handshake -> (address, port)"""
+ if self.af == socket.AF_INET:
+- host, port = parse227(self.sendcmd('PASV'))
++ untrusted_host, port = parse227(self.sendcmd('PASV'))
++ if self.trust_server_pasv_ipv4_address:
++ host = untrusted_host
++ else:
++ host = self.sock.getpeername()[0]
+ else:
+ host, port = parse229(self.sendcmd('EPSV'), self.sock.getpeername())
+ return host, port
+diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py
+index 8a3eb067a4..f3217d686d 100644
+--- a/Lib/test/test_ftplib.py
++++ b/Lib/test/test_ftplib.py
+@@ -67,6 +67,10 @@ class DummyFTPHandler(asynchat.async_chat):
+ self.rest = None
+ self.next_retr_data = RETR_DATA
+ self.push('220 welcome')
++ # We use this as the string IPv4 address to direct the client
++ # to in response to a PASV command. To test security behavior.
++ # https://bugs.python.org/issue43285/.
++ self.fake_pasv_server_ip = '252.253.254.255'
+
+ def collect_incoming_data(self, data):
+ self.in_buffer.append(data)
+@@ -109,8 +113,9 @@ class DummyFTPHandler(asynchat.async_chat):
+ sock.bind((self.socket.getsockname()[0], 0))
+ sock.listen(5)
+ sock.settimeout(10)
+- ip, port = sock.getsockname()[:2]
+- ip = ip.replace('.', ',')
++ port = sock.getsockname()[1]
++ ip = self.fake_pasv_server_ip
++ ip = ip.replace('.', ','); p1 = port // 256; p2 = port % 256
+ p1, p2 = divmod(port, 256)
+ self.push('227 entering passive mode (%s,%d,%d)' %(ip, p1, p2))
+ conn, addr = sock.accept()
+@@ -577,6 +582,26 @@ class TestFTPClass(TestCase):
+ # IPv4 is in use, just make sure send_epsv has not been used
+ self.assertEqual(self.server.handler_instance.last_received_cmd, 'pasv')
+
++ def test_makepasv_issue43285_security_disabled(self):
++ """Test the opt-in to the old vulnerable behavior."""
++ self.client.trust_server_pasv_ipv4_address = True
++ bad_host, port = self.client.makepasv()
++ self.assertEqual(
++ bad_host, self.server.handler_instance.fake_pasv_server_ip)
++ # Opening and closing a connection keeps the dummy server happy
++ # instead of timing out on accept.
++ socket.create_connection((self.client.sock.getpeername()[0], port),
++ timeout=TIMEOUT).close()
++
++ def test_makepasv_issue43285_security_enabled_default(self):
++ self.assertFalse(self.client.trust_server_pasv_ipv4_address)
++ trusted_host, port = self.client.makepasv()
++ self.assertNotEqual(
++ trusted_host, self.server.handler_instance.fake_pasv_server_ip)
++ # Opening and closing a connection keeps the dummy server happy
++ # instead of timing out on accept.
++ socket.create_connection((trusted_host, port), timeout=TIMEOUT).close()
++
+ def test_line_too_long(self):
+ self.assertRaises(ftplib.Error, self.client.sendcmd,
+ 'x' * self.client.maxline * 2)
+diff --git a/Misc/NEWS.d/next/Security/2021-03-13-03-48-14.bpo-43285.g-Hah3.rst b/Misc/NEWS.d/next/Security/2021-03-13-03-48-14.bpo-43285.g-Hah3.rst
+new file mode 100644
+index 0000000000..8312b7e885
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2021-03-13-03-48-14.bpo-43285.g-Hah3.rst
+@@ -0,0 +1,8 @@
++:mod:`ftplib` no longer trusts the IP address value returned from the server
++in response to the PASV command by default. This prevents a malicious FTP
++server from using the response to probe IPv4 address and port combinations
++on the client network.
++
++Code that requires the former vulnerable behavior may set a
++``trust_server_pasv_ipv4_address`` attribute on their
++:class:`ftplib.FTP` instances to ``True`` to re-enable it.
+--
+2.38.1
+
diff --git a/CVE-2022-0391.patch b/CVE-2022-0391.patch
new file mode 100644
index 0000000..5c45417
--- /dev/null
+++ b/CVE-2022-0391.patch
@@ -0,0 +1,151 @@
+From 76cd81d60310d65d01f9d7b48a8985d8ab89c8b4 Mon Sep 17 00:00:00 2001
+From: Senthil Kumaran <senthil at uthcode.com>
+Date: Thu, 29 Apr 2021 10:16:50 -0700
+Subject: [PATCH] bpo-43882 - urllib.parse should sanitize urls containing
+ ASCII newline and tabs. (GH-25595)
+
+* issue43882 - urllib.parse should sanitize urls containing ASCII newline and tabs.
+
+Co-authored-by: Gregory P. Smith <greg at krypto.org>
+Co-authored-by: Serhiy Storchaka <storchaka at gmail.com>
+---
+ Doc/library/urllib.parse.rst | 13 +++++++++
+ Lib/test/test_urlparse.py | 29 +++++++++++++++++++
+ Lib/urllib/parse.py | 6 ++++
+ .../2021-04-25-07-46-37.bpo-43882.Jpwx85.rst | 6 ++++
+ 4 files changed, 54 insertions(+)
+ create mode 100644 Misc/NEWS.d/next/Security/2021-04-25-07-46-37.bpo-43882.Jpwx85.rst
+
+From 985ac016373403e8ad41f8d563c4355ffa8d49ff Mon Sep 17 00:00:00 2001
+From: Senthil Kumaran <senthil at uthcode.com>
+Date: Wed, 5 May 2021 15:50:05 -0700
+Subject: [PATCH] bpo-43882 Remove the newline, and tab early. From query and
+ fragments. (GH-25921)
+
+---
+ Lib/test/test_urlparse.py | 24 ++++++++++++++++--------
+ Lib/urllib/parse.py | 8 +++++---
+ 2 files changed, 21 insertions(+), 11 deletions(-)
+
+Backport:
+ * Drop Misc/NEWS.d
+ * urllib.parse -> urlparse
+ * Update hunk context
+
+diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst
+index 67c2120819..0aaac56288 100644
+--- a/Doc/library/urlparse.rst
++++ b/Doc/library/urlparse.rst
+@@ -312,6 +312,9 @@ or on combining URL components into a URL string.
+ decomposed before parsing, or is not a Unicode string, no error will be
+ raised.
+
++ Following the `WHATWG spec`_ that updates RFC 3986, ASCII newline
++ ``\n``, ``\r`` and tab ``\t`` characters are stripped from the URL.
++
+ .. versionadded:: 2.2
+
+ .. versionchanged:: 2.5
+@@ -320,6 +323,10 @@ or on combining URL components into a URL string.
+ Characters that affect netloc parsing under NFKC normalization will
+ now raise :exc:`ValueError`.
+
++ .. versionchanged:: 2.7 security update
++ ASCII newline and tab characters are stripped from the URL.
++
++.. _WHATWG spec: https://url.spec.whatwg.org/#concept-basic-url-parser
+
+ .. function:: urlunsplit(parts)
+
+@@ -674,6 +681,10 @@ task isn't already covered by the URL parsing functions above.
+
+ .. seealso::
+
++ `WHATWG`_ - URL Living standard
++ Working Group for the URL Standard that defines URLs, domains, IP addresses, the
++ application/x-www-form-urlencoded format, and their API.
++
+ :rfc:`3986` - Uniform Resource Identifiers
+ This is the current standard (STD66). Any changes to urlparse module
+ should conform to this. Certain deviations could be observed, which are
+@@ -697,6 +708,8 @@ task isn't already covered by the URL parsing functions above.
+
+ :rfc:`1738` - Uniform Resource Locators (URL)
+ This specifies the formal syntax and semantics of absolute URLs.
++
++.. _WHATWG: https://url.spec.whatwg.org/
+
+
+ .. _urlparse-result-object:
+diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py
+--- a/Lib/test/test_urlparse.py
++++ b/Lib/test/test_urlparse.py
+@@ -612,6 +612,43 @@
+ self.assertEqual(p1.params, 'phone-context=+1-914-555')
+
+
++ def test_urlsplit_remove_unsafe_bytes(self):
++ # Remove ASCII tabs and newlines from input
++ url = "http\t://www.python\n.org\t/java\nscript:\talert('msg\r\n')/?query\n=\tsomething#frag\nment"
++ p = urlparse.urlsplit(url)
++ self.assertEqual(p.scheme, "http")
++ self.assertEqual(p.netloc, "www.python.org")
++ self.assertEqual(p.path, "/javascript:alert('msg')/")
++ self.assertEqual(p.query, "query=something")
++ self.assertEqual(p.fragment, "fragment")
++ self.assertEqual(p.username, None)
++ self.assertEqual(p.password, None)
++ self.assertEqual(p.hostname, "www.python.org")
++ self.assertEqual(p.port, None)
++ self.assertEqual(p.geturl(), "http://www.python.org/javascript:alert('msg')/?query=something#fragment")
++
++ # Remove ASCII tabs and newlines from input as bytes.
++ url = b"http\t://www.python\n.org\t/java\nscript:\talert('msg\r\n')/?query\n=\tsomething#frag\nment"
++ p = urlparse.urlsplit(url)
++ self.assertEqual(p.scheme, b"http")
++ self.assertEqual(p.netloc, b"www.python.org")
++ self.assertEqual(p.path, b"/javascript:alert('msg')/")
++ self.assertEqual(p.query, b"query=something")
++ self.assertEqual(p.fragment, b"fragment")
++ self.assertEqual(p.username, None)
++ self.assertEqual(p.password, None)
++ self.assertEqual(p.hostname, b"www.python.org")
++ self.assertEqual(p.port, None)
++ self.assertEqual(p.geturl(), b"http://www.python.org/javascript:alert('msg')/?query=something#fragment")
++
++ # with scheme as cache-key
++ url = "http://www.python.org/java\nscript:\talert('msg\r\n')/?query\n=\tsomething#frag\nment"
++ scheme = "ht\ntp"
++ for _ in range(2):
++ p = urlparse.urlsplit(url, scheme=scheme)
++ self.assertEqual(p.scheme, "http")
++ self.assertEqual(p.geturl(), "http://www.python.org/javascript:alert('msg')/?query=something#fragment")
++
+ def test_attributes_bad_port(self):
+ """Check handling of non-integer ports."""
+ p = urlparse.urlsplit("http://www.example.net:foo")
+diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py
+--- a/Lib/urlparse.py
++++ b/Lib/urlparse.py
+@@ -78,6 +78,9 @@
+ '0123456789'
+ '+-.')
+
++# Unsafe bytes to be removed per WHATWG spec
++_UNSAFE_URL_BYTES_TO_REMOVE = ['\t', '\r', '\n']
++
+ MAX_CACHE_SIZE = 20
+ _parse_cache = {}
+
+@@ -456,6 +456,11 @@ def urlsplit(url, scheme='', allow_fragments=True):
+ Return a 5-tuple: (scheme, netloc, path, query, fragment).
+ Note that we don't break the components up in smaller bits
+ (e.g. netloc is a single string) and we don't expand % escapes."""
++
++ for b in _UNSAFE_URL_BYTES_TO_REMOVE:
++ url = url.replace(b, "")
++ scheme = scheme.replace(b, "")
++
+ allow_fragments = bool(allow_fragments)
+ key = url, scheme, allow_fragments, type(url), type(scheme)
+ cached = _parse_cache.get(key, None)
diff --git a/CVE-2022-45061.patch b/CVE-2022-45061.patch
new file mode 100644
index 0000000..dbcdc03
--- /dev/null
+++ b/CVE-2022-45061.patch
@@ -0,0 +1,118 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington at users.noreply.github.com>
+Date: Mon, 7 Nov 2022 19:22:14 -0800
+Subject: [PATCH]
+ 00394-cve-2022-45061-cpu-denial-of-service-via-inefficient-idna-decoder.patch
+
+00394 #
+gh-98433: Fix quadratic time idna decoding.
+
+There was an unnecessary quadratic loop in idna decoding. This restores
+the behavior to linear.
+
+Backported from python3.
+
+(cherry picked from commit a6f6c3a3d6f2b580f2d87885c9b8a9350ad7bf15)
+
+Co-authored-by: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
+Co-authored-by: Gregory P. Smith <greg at krypto.org>
+---
+ Lib/encodings/idna.py | 32 +++++++++----------
+ Lib/test/test_codecs.py | 6 ++++
+ ...2-11-04-09-29-36.gh-issue-98433.l76c5G.rst | 6 ++++
+ 3 files changed, 27 insertions(+), 17 deletions(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2022-11-04-09-29-36.gh-issue-98433.l76c5G.rst
+
+diff --git a/Lib/encodings/idna.py b/Lib/encodings/idna.py
+index ea90d67142f..2ce798cf47e 100644
+--- a/Lib/encodings/idna.py
++++ b/Lib/encodings/idna.py
+@@ -39,23 +39,21 @@ def nameprep(label):
+
+ # Check bidi
+ RandAL = map(stringprep.in_table_d1, label)
+- for c in RandAL:
+- if c:
+- # There is a RandAL char in the string. Must perform further
+- # tests:
+- # 1) The characters in section 5.8 MUST be prohibited.
+- # This is table C.8, which was already checked
+- # 2) If a string contains any RandALCat character, the string
+- # MUST NOT contain any LCat character.
+- if filter(stringprep.in_table_d2, label):
+- raise UnicodeError("Violation of BIDI requirement 2")
+-
+- # 3) If a string contains any RandALCat character, a
+- # RandALCat character MUST be the first character of the
+- # string, and a RandALCat character MUST be the last
+- # character of the string.
+- if not RandAL[0] or not RandAL[-1]:
+- raise UnicodeError("Violation of BIDI requirement 3")
++ if any(RandAL):
++ # There is a RandAL char in the string. Must perform further
++ # tests:
++ # 1) The characters in section 5.8 MUST be prohibited.
++ # This is table C.8, which was already checked
++ # 2) If a string contains any RandALCat character, the string
++ # MUST NOT contain any LCat character.
++ if any(stringprep.in_table_d2(x) for x in label):
++ raise UnicodeError("Violation of BIDI requirement 2")
++ # 3) If a string contains any RandALCat character, a
++ # RandALCat character MUST be the first character of the
++ # string, and a RandALCat character MUST be the last
++ # character of the string.
++ if not RandAL[0] or not RandAL[-1]:
++ raise UnicodeError("Violation of BIDI requirement 3")
+
+ return label
+
+diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py
+index 0ec8bf5a4b4..76428e1794a 100644
+--- a/Lib/test/test_codecs.py
++++ b/Lib/test/test_codecs.py
+@@ -1318,6 +1318,12 @@ class IDNACodecTest(unittest.TestCase):
+ self.assertEqual(u"pyth\xf6n.org".encode("idna"), "xn--pythn-mua.org")
+ self.assertEqual(u"pyth\xf6n.org.".encode("idna"), "xn--pythn-mua.org.")
+
++ def test_builtin_decode_length_limit(self):
++ with self.assertRaisesRegexp(UnicodeError, "too long"):
++ (b"xn--016c"+b"a"*1100).decode("idna")
++ with self.assertRaisesRegexp(UnicodeError, "too long"):
++ (b"xn--016c"+b"a"*70).decode("idna")
++
+ def test_stream(self):
+ import StringIO
+ r = codecs.getreader("idna")(StringIO.StringIO("abc"))
+diff --git a/Misc/NEWS.d/next/Security/2022-11-04-09-29-36.gh-issue-98433.l76c5G.rst b/Misc/NEWS.d/next/Security/2022-11-04-09-29-36.gh-issue-98433.l76c5G.rst
+new file mode 100644
+index 00000000000..5185fac2e29
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2022-11-04-09-29-36.gh-issue-98433.l76c5G.rst
+@@ -0,0 +1,6 @@
++The IDNA codec decoder used on DNS hostnames by :mod:`socket` or :mod:`asyncio`
++related name resolution functions no longer involves a quadratic algorithm.
++This prevents a potential CPU denial of service if an out-of-spec excessive
++length hostname involving bidirectional characters were decoded. Some protocols
++such as :mod:`urllib` http ``3xx`` redirects potentially allow for an attacker
++to supply such a name.
+diff -urNp a/Lib/encodings/idna.py b/Lib/encodings/idna.py
+--- a/Lib/encodings/idna.py 2023-02-16 08:58:06.884171667 +0100
++++ b/Lib/encodings/idna.py 2023-02-16 08:59:31.931296399 +0100
+@@ -101,6 +101,16 @@ def ToASCII(label):
+ raise UnicodeError("label empty or too long")
+
+ def ToUnicode(label):
++ if len(label) > 1024:
++ # Protection from https://github.com/python/cpython/issues/98433.
++ # https://datatracker.ietf.org/doc/html/rfc5894#section-6
++ # doesn't specify a label size limit prior to NAMEPREP. But having
++ # one makes practical sense.
++ # This leaves ample room for nameprep() to remove Nothing characters
++ # per https://www.rfc-editor.org/rfc/rfc3454#section-3.1 while still
++ # preventing us from wasting time decoding a big thing that'll just
++ # hit the actual <= 63 length limit in Step 6.
++ raise UnicodeError("label way too long")
+ # Step 1: Check for ASCII
+ if isinstance(label, str):
+ pure_ascii = True
diff --git a/CVE-2022-48560.patch b/CVE-2022-48560.patch
new file mode 100644
index 0000000..26fdcad
--- /dev/null
+++ b/CVE-2022-48560.patch
@@ -0,0 +1,155 @@
+From 79f89e6e5a659846d1068e8b1bd8e491ccdef861 Mon Sep 17 00:00:00 2001
+From: Pablo Galindo <Pablogsal at gmail.com>
+Date: Thu, 23 Jan 2020 14:07:05 +0000
+Subject: [PATCH] bpo-39421: Fix posible crash in heapq with custom comparison
+ operators (GH-18118)
+
+* bpo-39421: Fix posible crash in heapq with custom comparison operators
+
+* fixup! bpo-39421: Fix posible crash in heapq with custom comparison operators
+
+* fixup! fixup! bpo-39421: Fix posible crash in heapq with custom comparison operators
+---
+ Lib/test/test_heapq.py | 31 ++++++++++++++++
+ .../2020-01-22-15-53-37.bpo-39421.O3nG7u.rst | 2 ++
+ Modules/_heapqmodule.c | 35 ++++++++++++++-----
+ 3 files changed, 59 insertions(+), 9 deletions(-)
+ create mode 100644 Misc/NEWS.d/next/Core and Builtins/2020-01-22-15-53-37.bpo-39421.O3nG7u.rst
+
+Backport:
+ * Drop Misc/NEWS.d
+ * test_heapq.py:
+ + Update hunk context
+ + list.clear() -> del list[:]
+ * _heapqmodule.c: Port the patch with significant changes
+ + PyObject_RichCompareBool -> cmp_lt
+ + X[Y] -> PyList_GET_ITEM(X, Y)
+ + 4th hunk: newitem refcount is already incremented, parent refcount extended
+
+diff --git a/Lib/test/test_heapq.py b/Lib/test/test_heapq.py
+index 861ba7540d..6902573e8f 100644
+--- a/Lib/test/test_heapq.py
++++ b/Lib/test/test_heapq.py
+@@ -432,6 +432,37 @@ def test_heappop_mutating_heap(self):
+ with self.assertRaises((IndexError, RuntimeError)):
+ self.module.heappop(heap)
+
++ def test_comparison_operator_modifiying_heap(self):
++ # See bpo-39421: Strong references need to be taken
++ # when comparing objects as they can alter the heap
++ class EvilClass(int):
++ def __lt__(self, o):
++ del heap[:]
++ return NotImplemented
++
++ heap = []
++ self.module.heappush(heap, EvilClass(0))
++ self.assertRaises(IndexError, self.module.heappushpop, heap, 1)
++
++ def test_comparison_operator_modifiying_heap_two_heaps(self):
++
++ class h(int):
++ def __lt__(self, o):
++ del list2[:]
++ return NotImplemented
++
++ class g(int):
++ def __lt__(self, o):
++ del list1[:]
++ return NotImplemented
++
++ list1, list2 = [], []
++
++ self.module.heappush(list1, h(0))
++ self.module.heappush(list2, g(0))
++
++ self.assertRaises((IndexError, RuntimeError), self.module.heappush, list1, g(1))
++ self.assertRaises((IndexError, RuntimeError), self.module.heappush, list2, h(1))
+
+ class TestErrorHandlingPython(TestErrorHandling):
+ module = py_heapq
+diff --git a/Modules/_heapqmodule.c b/Modules/_heapqmodule.c
+index a84cade3aa..6bc18b5f82 100644
+--- a/Modules/_heapqmodule.c
++++ b/Modules/_heapqmodule.c
+@@ -36,7 +36,11 @@ siftdown(PyListObject *heap, Py_ssize_t startpos, Py_ssize_t pos)
+ while (pos > startpos) {
+ parentpos = (pos - 1) >> 1;
+ parent = PyList_GET_ITEM(heap, parentpos);
++ Py_INCREF(newitem);
++ Py_INCREF(parent);
+ cmp = cmp_lt(newitem, parent);
++ Py_DECREF(parent);
++ Py_DECREF(newitem);
+ if (cmp == -1)
+ return -1;
+ if (size != PyList_GET_SIZE(heap)) {
+@@ -78,9 +82,13 @@ siftup(PyListObject *heap, Py_ssize_t pos)
+ childpos = 2*pos + 1; /* leftmost child position */
+ rightpos = childpos + 1;
+ if (rightpos < endpos) {
+- cmp = cmp_lt(
+- PyList_GET_ITEM(heap, childpos),
+- PyList_GET_ITEM(heap, rightpos));
++ PyObject* a = PyList_GET_ITEM(heap, childpos);
++ PyObject* b = PyList_GET_ITEM(heap, rightpos);
++ Py_INCREF(a);
++ Py_INCREF(b);
++ cmp = cmp_lt(a, b);
++ Py_DECREF(a);
++ Py_DECREF(b);
+ if (cmp == -1)
+ return -1;
+ if (cmp == 0)
+@@ -264,7 +271,10 @@ _heapq_heappushpop_impl(PyObject *module, PyObject *heap, PyObject *item)
+ return item;
+ }
+
+- cmp = cmp_lt(PyList_GET_ITEM(heap, 0), item);
++ PyObject* top = PyList_GET_ITEM(heap, 0);
++ Py_INCREF(top);
++ cmp = cmp_lt(top, item);
++ Py_DECREF(top);
+ if (cmp == -1)
+ return NULL;
+ if (cmp == 0) {
+@@ -420,14 +430,17 @@ siftdown_max(PyListObject *heap, Py_ssize_t startpos, Py_ssize_t pos)
+ while (pos > startpos){
+ parentpos = (pos - 1) >> 1;
+ parent = PyList_GET_ITEM(heap, parentpos);
++ Py_INCREF(parent);
+ cmp = cmp_lt(parent, newitem);
+ if (cmp == -1) {
++ Py_DECREF(parent);
+ Py_DECREF(newitem);
+ return -1;
+ }
+- if (cmp == 0)
++ if (cmp == 0) {
++ Py_DECREF(parent);
+ break;
++ }
+- Py_INCREF(parent);
+ Py_DECREF(PyList_GET_ITEM(heap, pos));
+ PyList_SET_ITEM(heap, pos, parent);
+ pos = parentpos;
+@@ -462,9 +476,13 @@ siftup_max(PyListObject *heap, Py_ssize_t pos)
+ childpos = 2*pos + 1; /* leftmost child position */
+ rightpos = childpos + 1;
+ if (rightpos < endpos) {
+- cmp = cmp_lt(
+- PyList_GET_ITEM(heap, rightpos),
+- PyList_GET_ITEM(heap, childpos));
++ PyObject* a = PyList_GET_ITEM(heap, rightpos);
++ PyObject* b = PyList_GET_ITEM(heap, childpos);
++ Py_INCREF(a);
++ Py_INCREF(b);
++ cmp = cmp_lt(a, b);
++ Py_DECREF(a);
++ Py_DECREF(b);
+ if (cmp == -1) {
+ Py_DECREF(newitem);
+ return -1;
+--
+2.40.1
+
diff --git a/CVE-2022-48565.patch b/CVE-2022-48565.patch
new file mode 100644
index 0000000..c2bc05a
--- /dev/null
+++ b/CVE-2022-48565.patch
@@ -0,0 +1,91 @@
+From dd9ccc8454250bb4c2e2fe517edbbbbe7d759e12 Mon Sep 17 00:00:00 2001
+From: "Miss Skeleton (bot)" <31488909+miss-islington at users.noreply.github.com>
+Date: Mon, 19 Oct 2020 21:38:30 -0700
+Subject: [PATCH 04/36] bpo-42051: Reject XML entity declarations in plist
+ files (GH-22760) (GH-22801) (GH-22804)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Co-authored-by: Ronald Oussoren <ronaldoussoren at mac.com>
+(cherry picked from commit e512bc799e3864fe3b1351757261762d63471efc)
+
+Co-authored-by: Ned Deily <nad at python.org>
+
+Rebased for Python 2.7 by Michał Górny <mgorny at gentoo.org>
+---
+ Lib/plistlib.py | 7 +++++++
+ Lib/test/test_plistlib.py | 18 ++++++++++++++++++
+ .../2020-10-19-10-56-27.bpo-42051.EU_B7u.rst | 3 +++
+ 3 files changed, 28 insertions(+)
+ create mode 100644 Misc/NEWS.d/next/Security/2020-10-19-10-56-27.bpo-42051.EU_B7u.rst
+
+diff --git a/Lib/plistlib.py b/Lib/plistlib.py
+index 42897b8da8..2c2b7fb635 100644
+--- a/Lib/plistlib.py
++++ b/Lib/plistlib.py
+@@ -403,9 +403,16 @@ class PlistParser:
+ parser.StartElementHandler = self.handleBeginElement
+ parser.EndElementHandler = self.handleEndElement
+ parser.CharacterDataHandler = self.handleData
++ parser.EntityDeclHandler = self.handleEntityDecl
+ parser.ParseFile(fileobj)
+ return self.root
+
++ def handleEntityDecl(self, entity_name, is_parameter_entity, value, base, system_id, public_id, notation_name):
++ # Reject plist files with entity declarations to avoid XML vulnerabilies in expat.
++ # Regular plist files don't contain those declerations, and Apple's plutil tool does not
++ # accept them either.
++ raise ValueError("XML entity declarations are not supported in plist files")
++
+ def handleBeginElement(self, element, attrs):
+ self.data = []
+ handler = getattr(self, "begin_" + element, None)
+diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py
+index 7859ad0572..612a1d2d6e 100644
+--- a/Lib/test/test_plistlib.py
++++ b/Lib/test/test_plistlib.py
+@@ -86,6 +86,19 @@ TESTDATA = """<?xml version="1.0" encoding="UTF-8"?>
+ </plist>
+ """.replace(" " * 8, "\t") # Apple as well as plistlib.py output hard tabs
+
++XML_PLIST_WITH_ENTITY=b'''\
++<?xml version="1.0" encoding="UTF-8"?>
++<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" [
++ <!ENTITY entity "replacement text">
++ ]>
++<plist version="1.0">
++ <dict>
++ <key>A</key>
++ <string>&entity;</string>
++ </dict>
++</plist>
++'''
++
+
+ class TestPlistlib(unittest.TestCase):
+
+@@ -195,6 +208,11 @@ class TestPlistlib(unittest.TestCase):
+ self.assertEqual(test1, result1)
+ self.assertEqual(test2, result2)
+
++ def test_xml_plist_with_entity_decl(self):
++ with self.assertRaisesRegexp(ValueError,
++ "XML entity declarations are not supported"):
++ plistlib.readPlistFromString(XML_PLIST_WITH_ENTITY)
++
+
+ def test_main():
+ test_support.run_unittest(TestPlistlib)
+diff --git a/Misc/NEWS.d/next/Security/2020-10-19-10-56-27.bpo-42051.EU_B7u.rst b/Misc/NEWS.d/next/Security/2020-10-19-10-56-27.bpo-42051.EU_B7u.rst
+new file mode 100644
+index 0000000000..e865ed12a0
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2020-10-19-10-56-27.bpo-42051.EU_B7u.rst
+@@ -0,0 +1,3 @@
++The :mod:`plistlib` module no longer accepts entity declarations in XML
++plist files to avoid XML vulnerabilities. This should not affect users as
++entity declarations are not used in regular plist files.
+--
+2.38.1
+
diff --git a/CVE-2022-48566.patch b/CVE-2022-48566.patch
new file mode 100644
index 0000000..657af20
--- /dev/null
+++ b/CVE-2022-48566.patch
@@ -0,0 +1,48 @@
+From bfc498a6c971c7393d37c25bdcf5f892afb16ed2 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington at users.noreply.github.com>
+Date: Sun, 22 Nov 2020 09:33:09 -0800
+Subject: [PATCH 06/36] bpo-40791: Make compare_digest more constant-time.
+ (GH-23438)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+The existing volatile `left`/`right` pointers guarantee that the reads will all occur, but does not guarantee that they will be _used_. So a compiler can still short-circuit the loop, saving e.g. the overhead of doing the xors and especially the overhead of the data dependency between `result` and the reads. That would change performance depending on where the first unequal byte occurs. This change removes that optimization.
+
+(This is change GH-1 from https://bugs.python.org/issue40791 .)
+(cherry picked from commit 31729366e2bc09632e78f3896dbce0ae64914f28)
+
+Co-authored-by: Devin Jeanpierre <jeanpierreda at google.com>
+
+Rebased for Python 2.7 by Michał Górny <mgorny at gentoo.org>
+---
+ .../next/Security/2020-05-28-06-06-47.bpo-40791.QGZClX.rst | 1 +
+ Modules/operator.c | 2 +-
+ 2 files changed, 2 insertions(+), 1 deletion(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2020-05-28-06-06-47.bpo-40791.QGZClX.rst
+
+diff --git a/Misc/NEWS.d/next/Security/2020-05-28-06-06-47.bpo-40791.QGZClX.rst b/Misc/NEWS.d/next/Security/2020-05-28-06-06-47.bpo-40791.QGZClX.rst
+new file mode 100644
+index 0000000000..69b9de1bea
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2020-05-28-06-06-47.bpo-40791.QGZClX.rst
+@@ -0,0 +1 @@
++Add ``volatile`` to the accumulator variable in ``hmac.compare_digest``, making constant-time-defeating optimizations less likely.
+\ No newline at end of file
+diff --git a/Modules/operator.c b/Modules/operator.c
+index 7ddd123f40..67011a6a82 100644
+--- a/Modules/operator.c
++++ b/Modules/operator.c
+@@ -259,7 +259,7 @@ _tscmp(const unsigned char *a, const unsigned char *b,
+ volatile const unsigned char *left;
+ volatile const unsigned char *right;
+ Py_ssize_t i;
+- unsigned char result;
++ volatile unsigned char result;
+
+ /* loop count depends on length of b */
+ length = len_b;
+--
+2.38.1
+
diff --git a/CVE-2023-24329.patch b/CVE-2023-24329.patch
new file mode 100644
index 0000000..2bd0ba4
--- /dev/null
+++ b/CVE-2023-24329.patch
@@ -0,0 +1,264 @@
+From 439b9cfaf43080e91c4ad69f312f21fa098befc7 Mon Sep 17 00:00:00 2001
+From: Ben Kallus <49924171+kenballus at users.noreply.github.com>
+Date: Sun, 13 Nov 2022 18:25:55 +0000
+Subject: [PATCH] gh-99418: Make urllib.parse.urlparse enforce that a scheme
+ must begin with an alphabetical ASCII character. (#99421)
+
+Prevent urllib.parse.urlparse from accepting schemes that don't begin with an alphabetical ASCII character.
+
+RFC 3986 defines a scheme like this: `scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )`
+RFC 2234 defines an ALPHA like this: `ALPHA = %x41-5A / %x61-7A`
+
+The WHATWG URL spec defines a scheme like this:
+`"A URL-scheme string must be one ASCII alpha, followed by zero or more of ASCII alphanumeric, U+002B (+), U+002D (-), and U+002E (.)."`
+---
+ Lib/test/test_urlparse.py | 18 ++++++++++++++++++
+ Lib/urllib/parse.py | 2 +-
+ ...22-11-12-15-45-51.gh-issue-99418.FxfAXS.rst | 2 ++
+ 3 files changed, 21 insertions(+), 1 deletion(-)
+ create mode 100644 Misc/NEWS.d/next/Library/2022-11-12-15-45-51.gh-issue-99418.FxfAXS.rst
+
+From 2f630e1ce18ad2e07428296532a68b11dc66ad10 Mon Sep 17 00:00:00 2001
+From: Illia Volochii <illia.volochii at gmail.com>
+Date: Wed, 17 May 2023 11:49:20 +0300
+Subject: [PATCH] gh-102153: Start stripping C0 control and space chars in
+ `urlsplit` (#102508)
+
+`urllib.parse.urlsplit` has already been respecting the WHATWG spec a bit #25595.
+
+This adds more sanitizing to respect the "Remove any leading C0 control or space from input" [rule](https://url.spec.whatwg.org/#url-parsing:~:text=Remove%20any%20leading%20and%20trailing%20C0%20control%20or%20space%20from%20input.) in response to [CVE-2023-24329](https://nvd.nist.gov/vuln/detail/CVE-2023-24329).
+
+---------
+
+Co-authored-by: Gregory P. Smith [Google] <greg at krypto.org>
+---
+ Doc/library/urllib.parse.rst | 46 +++++++++++++-
+ Lib/test/test_urlparse.py | 61 ++++++++++++++++++-
+ Lib/urllib/parse.py | 12 ++++
+ ...-03-07-20-59-17.gh-issue-102153.14CLSZ.rst | 3 +
+ 4 files changed, 119 insertions(+), 3 deletions(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst
+
+Backport:
+ * Drop Misc/NEWS.d
+ * urllib.parse -> urlparse
+ * Update hunk context
+ * Implement str.isascii
+ * test_urlparse.py:
+ * Various str vs bytes issues
+ * Drop hunk in test_attributes_bad_port
+ * Avoid using TestCase.subTest
+ * Don't use non-ascii in source
+
+diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py
+--- a/Lib/test/test_urlparse.py
++++ b/Lib/test/test_urlparse.py
+@@ -654,6 +654,65 @@ def test_urlsplit_remove_unsafe_bytes(self):
+ self.assertEqual(p.scheme, "http")
+ self.assertEqual(p.geturl(), "http://www.python.org/javascript:alert('msg')/?query=something#fragment")
+
++ def test_urlsplit_strip_url(self):
++ noise = "".join(map(chr, range(0, 0x20 + 1)))
++ base_url = "http://User:Pass@www.python.org:080/doc/?query=yes#frag"
++
++ url = (noise + base_url).decode("utf8")
++ p = urlparse.urlsplit(url)
++ self.assertEqual(p.scheme, u"http")
++ self.assertEqual(p.netloc, u"User:Pass at www.python.org:080")
++ self.assertEqual(p.path, u"/doc/")
++ self.assertEqual(p.query, u"query=yes")
++ self.assertEqual(p.fragment, u"frag")
++ self.assertEqual(p.username, u"User")
++ self.assertEqual(p.password, u"Pass")
++ self.assertEqual(p.hostname, u"www.python.org")
++ self.assertEqual(p.port, 80)
++ self.assertEqual(p.geturl(), base_url.decode("utf8"))
++
++ url = noise + base_url
++ p = urlparse.urlsplit(url)
++ self.assertEqual(p.scheme, b"http")
++ self.assertEqual(p.netloc, b"User:Pass at www.python.org:080")
++ self.assertEqual(p.path, b"/doc/")
++ self.assertEqual(p.query, b"query=yes")
++ self.assertEqual(p.fragment, b"frag")
++ self.assertEqual(p.username, b"User")
++ self.assertEqual(p.password, b"Pass")
++ self.assertEqual(p.hostname, b"www.python.org")
++ self.assertEqual(p.port, 80)
++ self.assertEqual(p.geturl(), base_url)
++
++ # Test that trailing space is preserved as some applications rely on
++ # this within query strings.
++ query_spaces_url = "https://www.python.org:88/doc/?query= "
++ p = urlparse.urlsplit(noise + query_spaces_url)
++ self.assertEqual(p.scheme, "https")
++ self.assertEqual(p.netloc, "www.python.org:88")
++ self.assertEqual(p.path, "/doc/")
++ self.assertEqual(p.query, "query= ")
++ self.assertEqual(p.port, 88)
++ self.assertEqual(p.geturl(), query_spaces_url)
++
++ p = urlparse.urlsplit("www.pypi.org ")
++ # That "hostname" gets considered a "path" due to the
++ # trailing space and our existing logic... YUCK...
++ # and re-assembles via geturl aka unurlsplit into the original.
++ # django.core.validators.URLValidator (at least through v3.2) relies on
++ # this, for better or worse, to catch it in a ValidationError via its
++ # regular expressions.
++ # Here we test the basic round trip concept of such a trailing space.
++ self.assertEqual(urlparse.urlunsplit(p), "www.pypi.org ")
++
++ # with scheme as cache-key
++ url = "//www.python.org/"
++ scheme = noise + "https" + noise
++ for _ in range(2):
++ p = urlparse.urlsplit(url, scheme=scheme)
++ self.assertEqual(p.scheme, "https")
++ self.assertEqual(p.geturl(), "https://www.python.org/")
++
+ def test_attributes_bad_port(self):
+ """Check handling of non-integer ports."""
+ p = urlparse.urlsplit("http://www.example.net:foo")
+@@ -668,6 +668,23 @@ def test_attributes_bad_port(self):
+ self.assertEqual(p.netloc, "www.example.net:foo")
+ self.assertRaises(ValueError, lambda: p.port)
+
++ def test_attributes_bad_scheme(self):
++ """Check handling of invalid schemes."""
++ for bytes in (False, True):
++ for parse in (urlparse.urlsplit, urlparse.urlparse):
++ for scheme in (u".", u"+", u"-", u"0", u"http&", u"\xe0http"):
++ url = scheme + u"://www.example.net"
++ if bytes:
++ if all(ord(c) < 128 for c in url):
++ url = url.encode("ascii")
++ else:
++ continue
++ p = parse(url)
++ if bytes:
++ self.assertEqual(p.scheme, b"")
++ else:
++ self.assertEqual(p.scheme, u"")
++
+ def test_attributes_without_netloc(self):
+ # This example is straight from RFC 3261. It looks like it
+ # should allow the username, hostname, and port to be filled
+diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py
+index 9a3102afd6..4f6867accb 100644
+--- a/Lib/urlparse.py
++++ b/Lib/urlparse.py
+@@ -25,6 +25,10 @@
+ scenarios for parsing, and for backward compatibility purposes, some
+ parsing quirks from older RFCs are retained. The testcases in
+ test_urlparse.py provides a good indicator of parsing behavior.
++
++The WHATWG URL Parser spec should also be considered. We are not compliant with
++it either due to existing user code API behavior expectations (Hyrum's Law).
++It serves as a useful guide when making changes.
+
+ """
+
+@@ -80,6 +84,10 @@
+ '0123456789'
+ '+-.')
+
++# Leading and trailing C0 control and space to be stripped per WHATWG spec.
++# == "".join([chr(i) for i in range(0, 0x20 + 1)])
++_WHATWG_C0_CONTROL_OR_SPACE = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f '
++
+ # Unsafe bytes to be removed per WHATWG spec
+ _UNSAFE_URL_BYTES_TO_REMOVE = ['\t', '\r', '\n']
+
+@@ -464,9 +472,13 @@ def urlsplit(url, scheme='', allow_fragments=True):
+ return cached
+ if len(_parse_cache) >= MAX_CACHE_SIZE: # avoid runaway growth
+ clear_cache()
++ # Only lstrip url as some applications rely on preserving trailing space.
++ # (https://url.spec.whatwg.org/#concept-basic-url-parser would strip both)
++ url = url.lstrip(_WHATWG_C0_CONTROL_OR_SPACE)
++ scheme = scheme.strip(_WHATWG_C0_CONTROL_OR_SPACE)
+ netloc = query = fragment = ''
+ i = url.find(':')
+- if i > 0:
++ if i > 0 and ord(url[0]) < 128 and url[0].isalpha():
+ if url[:i] == 'http': # optimize the common case
+ scheme = url[:i].lower()
+ url = url[i+1:]
+diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst
+index 96b3965107..5a9a53f83d 100644
+--- a/Doc/library/urlparse.rst
++++ b/Doc/library/urlparse.rst
+@@ -159,6 +159,11 @@ or on combining URL components into a URL string.
+ decomposed before parsing, or is not a Unicode string, no error will be
+ raised.
+
++ .. warning::
++
++ :func:`urlparse` does not perform validation. See :ref:`URL parsing
++ security <url-parsing-security>` for details.
++
+ .. versionchanged:: 2.5
+ Added attributes to return value.
+
+@@ -324,6 +328,15 @@ or on combining URL components into a URL string.
+ Following the `WHATWG spec`_ that updates RFC 3986, ASCII newline
+ ``\n``, ``\r`` and tab ``\t`` characters are stripped from the URL.
+
++ Following some of the `WHATWG spec`_ that updates RFC 3986, leading C0
++ control and space characters are stripped from the URL. ``\n``,
++ ``\r`` and tab ``\t`` characters are removed from the URL at any position.
++
++ .. warning::
++
++ :func:`urlsplit` does not perform validation. See :ref:`URL parsing
++ security <url-parsing-security>` for details.
++
+ .. versionadded:: 2.2
+
+ .. versionchanged:: 2.5
+@@ -338,6 +348,9 @@ or on combining URL components into a URL string.
+ .. versionchanged:: 2.7 security update
+ ASCII newline and tab characters are stripped from the URL.
+
++ .. versionchanged:: 2.7 security update
++ Leading WHATWG C0 control and space characters are stripped from the URL.
++
+ .. _WHATWG spec: https://url.spec.whatwg.org/#concept-basic-url-parser
+
+ .. function:: urlunsplit(parts)
+@@ -414,6 +427,35 @@ or on combining URL components into a URL string.
+ .. _WHATWG: https://url.spec.whatwg.org/
+
+
++.. _url-parsing-security:
++
++URL parsing security
++--------------------
++
++The :func:`urlsplit` and :func:`urlparse` APIs do not perform **validation** of
++inputs. They may not raise errors on inputs that other applications consider
++invalid. They may also succeed on some inputs that might not be considered
++URLs elsewhere. Their purpose is for practical functionality rather than
++purity.
++
++Instead of raising an exception on unusual input, they may instead return some
++component parts as empty strings. Or components may contain more than perhaps
++they should.
++
++We recommend that users of these APIs where the values may be used anywhere
++with security implications code defensively. Do some verification within your
++code before trusting a returned component part. Does that ``scheme`` make
++sense? Is that a sensible ``path``? Is there anything strange about that
++``hostname``? etc.
++
++What constitutes a URL is not universally well defined. Different applications
++have different needs and desired constraints. For instance the living `WHATWG
++spec`_ describes what user facing web clients such as a web browser require.
++While :rfc:`3986` is more general. These functions incorporate some aspects of
++both, but cannot be claimed compliant with either. The APIs and existing user
++code with expectations on specific behaviors predate both standards leading us
++to be very cautious about making API behavior changes.
++
+ .. _urlparse-result-object:
+
+ Results of :func:`urlparse` and :func:`urlsplit`
diff --git a/CVE-2023-40217.patch b/CVE-2023-40217.patch
new file mode 100644
index 0000000..dd780be
--- /dev/null
+++ b/CVE-2023-40217.patch
@@ -0,0 +1,321 @@
+From 0cb0c238d520a8718e313b52cffc356a5a7561bf Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?=C5=81ukasz=20Langa?= <lukasz at langa.pl>
+Date: Tue, 22 Aug 2023 19:53:15 +0200
+Subject: [PATCH] gh-108310: Fix CVE-2023-40217: Check for & avoid the ssl
+ pre-close flaw (#108315)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Instances of `ssl.SSLSocket` were vulnerable to a bypass of the TLS handshake
+and included protections (like certificate verification) and treating sent
+unencrypted data as if it were post-handshake TLS encrypted data.
+
+The vulnerability is caused when a socket is connected, data is sent by the
+malicious peer and stored in a buffer, and then the malicious peer closes the
+socket within a small timing window before the other peers’ TLS handshake can
+begin. After this sequence of events the closed socket will not immediately
+attempt a TLS handshake due to not being connected but will also allow the
+buffered data to be read as if a successful TLS handshake had occurred.
+
+Co-authored-by: Gregory P. Smith [Google LLC] <greg at krypto.org>
+---
+ Lib/ssl.py | 31 ++-
+ Lib/test/test_ssl.py | 211 ++++++++++++++++++
+ ...-08-22-17-39-12.gh-issue-108310.fVM3sg.rst | 7 +
+ 3 files changed, 248 insertions(+), 1 deletion(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst
+
+From 64f99350351bc46e016b2286f36ba7cd669b79e3 Mon Sep 17 00:00:00 2001
+From: Victor Stinner <vstinner at python.org>
+Date: Wed, 23 Aug 2023 07:26:01 +0200
+Subject: [PATCH] gh-108342: Break ref cycle in SSLSocket._create() exc
+ (#108344)
+
+Explicitly break a reference cycle when SSLSocket._create() raises an
+exception. Clear the variable storing the exception, since the
+exception traceback contains the variables and so creates a reference
+cycle.
+
+This test leak was introduced by the test added for the fix of #108310.
+---
+ Lib/ssl.py | 6 +++++-
+ 1 file changed, 5 insertions(+), 1 deletion(-)
+
+Backport:
+ * ssl.py:
+ * There is no socket.getblocking. In the connected case, we do not retain the
+ timeout, because #8524 was never fixed on Python 2.7. In the disconnected
+ case, we have to modify the timeout and therefore restore it.
+ * Cannot use self.recv in c'tor, because it would access self._sslobj
+ * socket.socket raises socket.error rather than OSError
+ * test_ssl.py:
+ * Merge imports
+ * Inline socket_helper.bind_port
+ * Delete non_linux_skip_if_other_okay_error: This backport is Linux-only
+ * socket.socket isn't a ContextManager yet
+ * http.client -> httplib
+ * Implement assertRaises
+ * Remove use of keyword-only arguments
+ * Do not use f"strings"
+ * Drop Misc/NEWS.d
+
+diff --git a/Lib/ssl.py b/Lib/ssl.py
+index 1d58737264..ff363c75e7 100644
+--- a/Lib/ssl.py
++++ b/Lib/ssl.py
+@@ -994,6 +994,38 @@ def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
+ if e.errno != errno.ENOTCONN:
+ raise
+ connected = False
++ sock_timeout = self.gettimeout()
++ self.settimeout(0)
++ try:
++ # We are not connected so this is not supposed to block, but
++ # testing revealed otherwise on macOS and Windows so we do
++ # the non-blocking dance regardless. Our raise when any data
++ # is found means consuming the data is harmless.
++ notconn_pre_handshake_data = self._sock.recv(1)
++ except socket_error as e:
++ # EINVAL occurs for recv(1) on non-connected on unix sockets.
++ if e.errno not in (errno.ENOTCONN, errno.EINVAL):
++ raise
++ notconn_pre_handshake_data = b''
++ self.settimeout(sock_timeout)
++ if notconn_pre_handshake_data:
++ # This prevents pending data sent to the socket before it was
++ # closed from escaping to the caller who could otherwise
++ # presume it came through a successful TLS connection.
++ reason = "Closed before TLS handshake with data in recv buffer."
++ notconn_pre_handshake_data_error = SSLError(e.errno, reason)
++ # Add the SSLError attributes that _ssl.c always adds.
++ notconn_pre_handshake_data_error.reason = reason
++ notconn_pre_handshake_data_error.library = None
++ try:
++ self.close()
++ except socket_error:
++ pass
++ try:
++ raise notconn_pre_handshake_data_error
++ finally:
++ # Explicitly break the reference cycle.
++ notconn_pre_handshake_data_error = None
+ else:
+ connected = True
+
+diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
+index 6117ca3fdb..ad5377ec05 100644
+--- a/Lib/test/test_ssl.py
++++ b/Lib/test/test_ssl.py
+@@ -10,9 +10,11 @@
+ import asyncore
+ import socket
+ import select
++import struct
+ import time
+ import datetime
+ import gc
++import httplib
+ import os
+ import errno
+ import pprint
+@@ -4659,5 +4662,196 @@ def sni_cb(sock, servername, ctx):
+ if _have_threads:
+ support.threading_cleanup(*thread_info)
+
++def set_socket_so_linger_on_with_zero_timeout(sock):
++ sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
++
++
++class TestPreHandshakeClose(unittest.TestCase):
++ """Verify behavior of close sockets with received data before to the handshake.
++ """
++
++ class SingleConnectionTestServerThread(threading.Thread):
++
++ def __init__(self, name, call_after_accept):
++ self.call_after_accept = call_after_accept
++ self.received_data = b'' # set by .run()
++ self.wrap_error = None # set by .run()
++ self.listener = None # set by .start()
++ self.port = None # set by .start()
++ super().__init__(name=name)
++
++ def __enter__(self):
++ self.start()
++ return self
++
++ def __exit__(self, *args):
++ try:
++ if self.listener:
++ self.listener.close()
++ except OSError:
++ pass
++ self.join()
++ self.wrap_error = None # avoid dangling references
++
++ def start(self):
++ self.ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
++ self.ssl_ctx.verify_mode = ssl.CERT_REQUIRED
++ self.ssl_ctx.load_verify_locations(cafile=ONLYCERT)
++ self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
++ self.listener = socket.socket()
++ self.listener.bind((None, 0))
++ self.port = self.listener.getsockname()[1]
++ self.listener.settimeout(2.0)
++ self.listener.listen(1)
++ super().start()
++
++ def run(self):
++ conn, address = self.listener.accept()
++ self.listener.close()
++ with conn:
++ if self.call_after_accept(conn):
++ return
++ try:
++ tls_socket = self.ssl_ctx.wrap_socket(conn, server_side=True)
++ except OSError as err: # ssl.SSLError inherits from OSError
++ self.wrap_error = err
++ else:
++ try:
++ self.received_data = tls_socket.recv(400)
++ except OSError:
++ pass # closed, protocol error, etc.
++
++ def test_preauth_data_to_tls_server(self):
++ server_accept_called = threading.Event()
++ ready_for_server_wrap_socket = threading.Event()
++
++ def call_after_accept(unused):
++ server_accept_called.set()
++ if not ready_for_server_wrap_socket.wait(2.0):
++ raise RuntimeError("wrap_socket event never set, test may fail.")
++ return False # Tell the server thread to continue.
++
++ server = self.SingleConnectionTestServerThread(
++ call_after_accept=call_after_accept,
++ name="preauth_data_to_tls_server")
++ self.enterContext(server) # starts it & unittest.TestCase stops it.
++
++ with closing(socket.socket()) as client:
++ client.connect(server.listener.getsockname())
++ # This forces an immediate connection close via RST on .close().
++ set_socket_so_linger_on_with_zero_timeout(client)
++ client.setblocking(False)
++
++ server_accept_called.wait()
++ client.send(b"DELETE /data HTTP/1.0\r\n\r\n")
++ client.close() # RST
++
++ ready_for_server_wrap_socket.set()
++ server.join()
++ wrap_error = server.wrap_error
++ self.assertEqual(b"", server.received_data)
++ self.assertIsInstance(wrap_error, OSError) # All platforms.
++ self.assertIsInstance(wrap_error, ssl.SSLError)
++ self.assertIn("before TLS handshake with data", wrap_error.args[1])
++ self.assertIn("before TLS handshake with data", wrap_error.reason)
++ self.assertNotEqual(0, wrap_error.args[0])
++ self.assertIsNone(wrap_error.library, msg="attr must exist")
++
++ def test_preauth_data_to_tls_client(self):
++ client_can_continue_with_wrap_socket = threading.Event()
++
++ def call_after_accept(conn_to_client):
++ # This forces an immediate connection close via RST on .close().
++ set_socket_so_linger_on_with_zero_timeout(conn_to_client)
++ conn_to_client.send(
++ b"HTTP/1.0 307 Temporary Redirect\r\n"
++ b"Location: https://example.com/someone-elses-server\r\n"
++ b"\r\n")
++ conn_to_client.close() # RST
++ client_can_continue_with_wrap_socket.set()
++ return True # Tell the server to stop.
++
++ server = self.SingleConnectionTestServerThread(
++ call_after_accept=call_after_accept,
++ name="preauth_data_to_tls_client")
++ self.enterContext(server) # starts it & unittest.TestCase stops it.
++ # Redundant; call_after_accept sets SO_LINGER on the accepted conn.
++ set_socket_so_linger_on_with_zero_timeout(server.listener)
++
++ with closing(socket.socket()) as client:
++ client.connect(server.listener.getsockname())
++ if not client_can_continue_with_wrap_socket.wait(2.0):
++ self.fail("test server took too long.")
++ ssl_ctx = ssl.create_default_context()
++ try:
++ tls_client = ssl_ctx.wrap_socket(
++ client, server_hostname="localhost")
++ except OSError as err: # SSLError inherits from OSError
++ wrap_error = err
++ received_data = b""
++ else:
++ wrap_error = None
++ received_data = tls_client.recv(400)
++ tls_client.close()
++
++ server.join()
++ self.assertEqual(b"", received_data)
++ self.assertIsInstance(wrap_error, OSError) # All platforms.
++ self.assertIsInstance(wrap_error, ssl.SSLError)
++ self.assertIn("before TLS handshake with data", wrap_error.args[1])
++ self.assertIn("before TLS handshake with data", wrap_error.reason)
++ self.assertNotEqual(0, wrap_error.args[0])
++ self.assertIsNone(wrap_error.library, msg="attr must exist")
++
++ def test_https_client_non_tls_response_ignored(self):
++
++ server_responding = threading.Event()
++
++ class SynchronizedHTTPSConnection(httplib.HTTPSConnection):
++ def connect(self):
++ httplib.HTTPConnection.connect(self)
++ # Wait for our fault injection server to have done its thing.
++ if not server_responding.wait(1.0) and support.verbose:
++ sys.stdout.write("server_responding event never set.")
++ self.sock = self._context.wrap_socket(
++ self.sock, server_hostname=self.host)
++
++ def call_after_accept(conn_to_client):
++ # This forces an immediate connection close via RST on .close().
++ set_socket_so_linger_on_with_zero_timeout(conn_to_client)
++ conn_to_client.send(
++ b"HTTP/1.0 402 Payment Required\r\n"
++ b"\r\n")
++ conn_to_client.close() # RST
++ server_responding.set()
++ return True # Tell the server to stop.
++
++ server = self.SingleConnectionTestServerThread(
++ call_after_accept=call_after_accept,
++ name="non_tls_http_RST_responder")
++ self.enterContext(server) # starts it & unittest.TestCase stops it.
++ # Redundant; call_after_accept sets SO_LINGER on the accepted conn.
++ set_socket_so_linger_on_with_zero_timeout(server.listener)
++
++ connection = SynchronizedHTTPSConnection(
++ "localhost",
++ port=server.port,
++ context=ssl.create_default_context(),
++ timeout=2.0,
++ )
++ # There are lots of reasons this raises as desired, long before this
++ # test was added. Sending the request requires a successful TLS wrapped
++ # socket; that fails if the connection is broken. It may seem pointless
++ # to test this. It serves as an illustration of something that we never
++ # want to happen... properly not happening.
++ try:
++ connection.request("HEAD", "/test", headers={"Host": "localhost"})
++ response = connection.getresponse()
++ except OSError:
++ pass
++ else:
++ self.fail("http request did not raise OSError")
++
++
+ if __name__ == "__main__":
+ test_main()
+--
+2.40.1
+
diff --git a/python-bpo-42278-pydoc-mktemp.patch b/python-bpo-42278-pydoc-mktemp.patch
new file mode 100644
index 0000000..938020b
--- /dev/null
+++ b/python-bpo-42278-pydoc-mktemp.patch
@@ -0,0 +1,59 @@
+From e5507ee1da8bb7a484311cf58238dd3daaae0aed Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington at users.noreply.github.com>
+Date: Sun, 29 Aug 2021 05:57:05 -0700
+Subject: [PATCH 15/36] bpo-42278: Use tempfile.TemporaryDirectory rather than
+ tempfile.mktemp in pydoc (GH-23200) (GH-28026)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Co-authored-by: Łukasz Langa <lukasz at langa.pl>
+(cherry picked from commit c9227df5a9d8e958a2324cf0deba8524d1ded26a)
+
+Co-authored-by: E-Paine <63801254+E-Paine at users.noreply.github.com>
+
+(updated for Python 2.7 by Michał Górny)
+---
+ Lib/pydoc.py | 11 ++++++-----
+ .../Security/2021-08-29-12-39-44.bpo-42278.jvmQz_.rst | 2 ++
+ 2 files changed, 8 insertions(+), 5 deletions(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2021-08-29-12-39-44.bpo-42278.jvmQz_.rst
+
+diff --git a/Lib/pydoc.py b/Lib/pydoc.py
+index 62cc262ccb..c2f2d31026 100755
+--- a/Lib/pydoc.py
++++ b/Lib/pydoc.py
+@@ -1423,15 +1423,16 @@ def pipepager(text, cmd):
+
+ def tempfilepager(text, cmd):
+ """Page through text by invoking a program on a temporary file."""
++ import shutil
+ import tempfile
+- filename = tempfile.mktemp()
+- file = open(filename, 'w')
+- file.write(_encode(text))
+- file.close()
++ tempdir = tempfile.mkdtemp()
+ try:
++ filename = os.path.join(tempdir, 'pydoc.out')
++ with open(filename, 'w') as file:
++ file.write(_encode(text))
+ os.system(cmd + ' "' + filename + '"')
+ finally:
+- os.unlink(filename)
++ shutil.rmtree(tempdir)
+
+ def ttypager(text):
+ """Page through text on a text terminal."""
+diff --git a/Misc/NEWS.d/next/Security/2021-08-29-12-39-44.bpo-42278.jvmQz_.rst b/Misc/NEWS.d/next/Security/2021-08-29-12-39-44.bpo-42278.jvmQz_.rst
+new file mode 100644
+index 0000000000..621c317670
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2021-08-29-12-39-44.bpo-42278.jvmQz_.rst
+@@ -0,0 +1,2 @@
++Replaced usage of :func:`tempfile.mktemp` with :func:`tempfile.mkdtemp`
++to avoid a potential race condition.
+--
+2.38.1
+
diff --git a/python-bpo-43124-smtplib-crlf.patch b/python-bpo-43124-smtplib-crlf.patch
new file mode 100644
index 0000000..d924b2d
--- /dev/null
+++ b/python-bpo-43124-smtplib-crlf.patch
@@ -0,0 +1,180 @@
+From 11afbbe796db6d50e5f31d12d32c0b9e1c79439b Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington at users.noreply.github.com>
+Date: Mon, 30 Aug 2021 12:21:57 -0700
+Subject: [PATCH 14/36] bpo-43124: Fix smtplib multiple CRLF injection
+ (GH-25987) (GH-28038)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Co-authored-by: Miguel Brito <5544985+miguendes at users.noreply.github.com>
+
+Co-authored-by: Łukasz Langa <lukasz at langa.pl>
+(cherry picked from commit 0897253f426068ea6a6fbe0ada01689af9ef1019)
+
+(updated for 2.7 by Michał Górny)
+---
+ Lib/smtplib.py | 12 +++-
+ Lib/test/test_smtplib.py | 65 +++++++++++++++++++
+ .../2021-05-08-11-50-46.bpo-43124.2CTM6M.rst | 2 +
+ 3 files changed, 76 insertions(+), 3 deletions(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
+
+diff --git a/Lib/smtplib.py b/Lib/smtplib.py
+index 0c61065432..b6c7285d8b 100755
+--- a/Lib/smtplib.py
++++ b/Lib/smtplib.py
+@@ -336,10 +336,16 @@ class SMTP:
+ def putcmd(self, cmd, args=""):
+ """Send a command to the server."""
+ if args == "":
+- str = '%s%s' % (cmd, CRLF)
++ s = cmd
+ else:
+- str = '%s %s%s' % (cmd, args, CRLF)
+- self.send(str)
++ s = '%s %s' % (cmd, args)
++ if '\r' in s or '\n' in s:
++ s = s.replace('\n', '\\n').replace('\r', '\\r')
++ raise ValueError(
++ 'command and arguments contain prohibited newline characters: %s'
++ % (s,)
++ )
++ self.send(s + CRLF)
+
+ def getreply(self):
+ """Get a reply from the server.
+diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py
+index 703b631c17..2224524ebd 100644
+--- a/Lib/test/test_smtplib.py
++++ b/Lib/test/test_smtplib.py
+@@ -155,6 +155,9 @@ class DebuggingServerTests(unittest.TestCase):
+ self._threads = test_support.threading_setup()
+ self.serv_evt = threading.Event()
+ self.client_evt = threading.Event()
++ # Capture SMTPChannel debug output
++ self.old_DEBUGSTREAM = smtpd.DEBUGSTREAM
++ smtpd.DEBUGSTREAM = StringIO.StringIO()
+ # Pick a random unused port by passing 0 for the port number
+ self.serv = smtpd.DebuggingServer((HOST, 0), ('nowhere', -1))
+ # Keep a note of what port was assigned
+@@ -176,6 +179,9 @@ class DebuggingServerTests(unittest.TestCase):
+ test_support.threading_cleanup(*self._threads)
+ # restore sys.stdout
+ sys.stdout = self.old_stdout
++ # restore DEBUGSTREAM
++ smtpd.DEBUGSTREAM.close()
++ smtpd.DEBUGSTREAM = self.old_DEBUGSTREAM
+
+ def testBasic(self):
+ # connect
+@@ -201,6 +207,17 @@ class DebuggingServerTests(unittest.TestCase):
+ self.assertEqual(smtp.ehlo(), expected)
+ smtp.quit()
+
++
++ def test_issue43124_putcmd_escapes_newline(self):
++ # see: https://bugs.python.org/issue43124
++ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
++ timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
++ self.addCleanup(smtp.close)
++ with self.assertRaises(ValueError) as exc:
++ smtp.putcmd('helo\nX-INJECTED')
++ self.assertIn("prohibited newline characters", str(exc.exception))
++ smtp.quit()
++
+ def testVRFY(self):
+ # VRFY isn't implemented in DebuggingServer
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
+@@ -240,6 +257,54 @@ class DebuggingServerTests(unittest.TestCase):
+ mexpect = '%s%s\n%s' % (MSG_BEGIN, m, MSG_END)
+ self.assertEqual(self.output.getvalue(), mexpect)
+
++ def test_issue43124_escape_localhostname(self):
++ # see: https://bugs.python.org/issue43124
++ # connect and send mail
++ m = 'wazzuuup\nlinetwo'
++ smtp = smtplib.SMTP(HOST, self.port, local_hostname='hi\nX-INJECTED',
++ timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
++ self.addCleanup(smtp.close)
++ with self.assertRaises(ValueError) as exc:
++ smtp.sendmail("hi at me.com", "you at me.com", m)
++ self.assertIn(
++ "prohibited newline characters: ehlo hi\\nX-INJECTED",
++ str(exc.exception),
++ )
++ # XXX (see comment in testSend)
++ time.sleep(0.01)
++ smtp.quit()
++
++ debugout = smtpd.DEBUGSTREAM.getvalue()
++ self.assertNotIn("X-INJECTED", debugout)
++
++ def test_issue43124_escape_options(self):
++ # see: https://bugs.python.org/issue43124
++ # connect and send mail
++ m = 'wazzuuup\nlinetwo'
++ smtp = smtplib.SMTP(
++ HOST, self.port, local_hostname='localhost',
++ timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
++
++ self.addCleanup(smtp.close)
++# smtp.sendmail("hi at me.com", "you at me.com", m)
++ # NB: gross hack but still cleaner than backporting whole ESMTP
++ # support to DebuggingServer
++ smtp.does_esmtp = 1
++ with self.assertRaises(ValueError) as exc:
++ smtp.mail("hi at me.com", ["X-OPTION\nX-INJECTED-1", "X-OPTION2\nX-INJECTED-2"])
++ msg = str(exc.exception)
++ self.assertIn("prohibited newline characters", msg)
++ self.assertIn("X-OPTION\\nX-INJECTED-1 X-OPTION2\\nX-INJECTED-2", msg)
++ # XXX (see comment in testSend)
++ time.sleep(0.01)
++ smtp.quit()
++
++ debugout = smtpd.DEBUGSTREAM.getvalue()
++ self.assertNotIn("X-OPTION", debugout)
++ self.assertNotIn("X-OPTION2", debugout)
++ self.assertNotIn("X-INJECTED-1", debugout)
++ self.assertNotIn("X-INJECTED-2", debugout)
++
+
+ class NonConnectingTests(unittest.TestCase):
+
+diff --git a/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst b/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
+new file mode 100644
+index 0000000000..e897d6cd36
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
+@@ -0,0 +1,2 @@
++Made the internal ``putcmd`` function in :mod:`smtplib` sanitize input for
++presence of ``\r`` and ``\n`` characters to avoid (unlikely) command injection.
+--
+2.38.1
+
+From 4e43d055206ca57794bdcaad2dac8978fd960663 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= <mgorny at gentoo.org>
+Date: Tue, 31 Aug 2021 10:50:53 +0200
+Subject: [PATCH 16/36] Fix accidentally leaving one sub-test commented out
+
+---
+ Lib/test/test_smtplib.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py
+index 2224524ebd..b6fa293228 100644
+--- a/Lib/test/test_smtplib.py
++++ b/Lib/test/test_smtplib.py
+@@ -286,7 +286,7 @@ class DebuggingServerTests(unittest.TestCase):
+ timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
+
+ self.addCleanup(smtp.close)
+-# smtp.sendmail("hi at me.com", "you at me.com", m)
++ smtp.sendmail("hi at me.com", "you at me.com", m)
+ # NB: gross hack but still cleaner than backporting whole ESMTP
+ # support to DebuggingServer
+ smtp.does_esmtp = 1
+--
+2.38.1
+
diff --git a/python-bpo-46756-urllib-auth.patch b/python-bpo-46756-urllib-auth.patch
new file mode 100644
index 0000000..133e1f3
--- /dev/null
+++ b/python-bpo-46756-urllib-auth.patch
@@ -0,0 +1,77 @@
+From e972fd923fd5172aa04c8f9d8775f1590ebbe394 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= <mgorny at gentoo.org>
+Date: Fri, 18 Mar 2022 12:26:44 +0100
+Subject: [PATCH 18/36] bpo-46756: Fix authorization check in urllib.request
+ (GH-31353)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Fix a bug in urllib.request.HTTPPasswordMgr.find_user_password() and
+urllib.request.HTTPPasswordMgrWithPriorAuth.is_authenticated() which
+allowed to bypass authorization. For example, access to URI "example.org/foobar"
+was allowed if the user was authorized for URI "example.org/foo".
+
+(rebased for 2.7 by Michał Górny)
+---
+ Lib/test/test_urllib2.py | 17 +++++++++++++++++
+ Lib/urllib2.py | 8 ++++----
+ 2 files changed, 21 insertions(+), 4 deletions(-)
+
+diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py
+index 0adbb13c43..6acab89678 100644
+--- a/Lib/test/test_urllib2.py
++++ b/Lib/test/test_urllib2.py
+@@ -137,6 +137,7 @@ def test_password_manager(self):
+ >>> add("Some Realm", "http://example.com/ni", "ni", "ni")
+ >>> add("c", "http://example.com/foo", "foo", "ni")
+ >>> add("c", "http://example.com/bar", "bar", "nini")
++ >>> add("c", "http://example.com/foo/bar", "foobar", "nibar")
+ >>> add("b", "http://example.com/", "first", "blah")
+ >>> add("b", "http://example.com/", "second", "spam")
+ >>> add("a", "http://example.com", "1", "a")
+@@ -158,6 +159,22 @@ def test_password_manager(self):
+ ('foo', 'ni')
+ >>> mgr.find_user_password("c", "http://example.com/bar")
+ ('bar', 'nini')
++ >>> mgr.find_user_password("c", "http://example.com/foo/")
++ ('foo', 'ni')
++ >>> mgr.find_user_password("c", "http://example.com/foo/bar")
++ ('foo', 'ni')
++ >>> mgr.find_user_password("c", "http://example.com/foo/baz")
++ ('foo', 'ni')
++ >>> mgr.find_user_password("c", "http://example.com/foobar")
++ (None, None)
++
++ >>> add("c", "http://example.com/baz/", "baz", "ninini")
++ >>> mgr.find_user_password("c", "http://example.com/baz")
++ (None, None)
++ >>> mgr.find_user_password("c", "http://example.com/baz/")
++ ('baz', 'ninini')
++ >>> mgr.find_user_password("c", "http://example.com/baz/bar")
++ ('baz', 'ninini')
+
+ Actually, this is really undefined ATM
+ ## Currently, we use the highest-level path where more than one match:
+diff --git a/Lib/urllib2.py b/Lib/urllib2.py
+index c92fb6afb8..f45ef57ee3 100644
+--- a/Lib/urllib2.py
++++ b/Lib/urllib2.py
+@@ -833,10 +833,10 @@ class HTTPPasswordMgr:
+ return True
+ if base[0] != test[0]:
+ return False
+- common = posixpath.commonprefix((base[1], test[1]))
+- if len(common) == len(base[1]):
+- return True
+- return False
++ prefix = base[1]
++ if prefix[-1:] != '/':
++ prefix += '/'
++ return test[1].startswith(prefix)
+
+
+ class HTTPPasswordMgrWithDefaultRealm(HTTPPasswordMgr):
+--
+2.38.1
+
diff --git a/python-configure-c99.patch b/python-configure-c99.patch
new file mode 100644
index 0000000..828be31
--- /dev/null
+++ b/python-configure-c99.patch
@@ -0,0 +1,52 @@
+From 351f5e0a4b805f78c8bb7e650ec695d7041d5f39 Mon Sep 17 00:00:00 2001
+From: Sam James <sam at gentoo.org>
+Date: Fri, 4 Nov 2022 03:38:19 +0000
+Subject: [PATCH 36/36] Fix -Wimplicit-int, -Wimplicit-function-declaration
+
+These warnings became fatal by default in Clang 16.
+
+Most of this has since been fixed upstream but
+https://github.com/python/cpython/pull/99085 is pending.
+
+Signed-off-by: Sam James <sam at gentoo.org>
+---
+ configure.ac | 5 ++++-
+ 1 file changed, 4 insertions(+), 1 deletion(-)
+
+diff --git a/configure.ac b/configure.ac
+index bf3b4e4d7e..0852fbd36a 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -2801,11 +2801,12 @@ if test "$posix_threads" = "yes"; then
+ AC_CACHE_VAL(ac_cv_pthread_system_supported,
+ [AC_RUN_IFELSE([AC_LANG_SOURCE([[
+ #include <stdio.h>
++ #include <stdlib.h>
+ #include <pthread.h>
+ void *foo(void *parm) {
+ return NULL;
+ }
+- main() {
++ int main() {
+ pthread_attr_t attr;
+ pthread_t id;
+ if (pthread_attr_init(&attr)) exit(-1);
+@@ -4164,6 +4165,7 @@ then
+ AC_MSG_CHECKING(whether wchar_t is signed)
+ AC_CACHE_VAL(ac_cv_wchar_t_signed, [
+ AC_RUN_IFELSE([AC_LANG_SOURCE([[
++ #include <stdlib.h>
+ #include <wchar.h>
+ int main()
+ {
+@@ -4253,6 +4255,7 @@ AC_C_BIGENDIAN
+ AC_MSG_CHECKING(whether right shift extends the sign bit)
+ AC_CACHE_VAL(ac_cv_rshift_extends_sign, [
+ AC_RUN_IFELSE([AC_LANG_SOURCE([[
++#include <stdlib.h>
+ int main()
+ {
+ exit(((-1)>>3 == -1) ? 0 : 1);
+--
+2.38.1
+
diff --git a/python-expat-2.4.5-tests.patch b/python-expat-2.4.5-tests.patch
new file mode 100644
index 0000000..973f7e7
--- /dev/null
+++ b/python-expat-2.4.5-tests.patch
@@ -0,0 +1,86 @@
+From 9f9f95976ac248d49685c39b355e0e1ce9001bca Mon Sep 17 00:00:00 2001
+From: Sebastian Pipping <sebastian at pipping.org>
+Date: Mon, 21 Feb 2022 15:48:32 +0100
+Subject: [PATCH 17/36] bpo-46811: Make test suite support Expat >=2.4.5
+ (GH-31453)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Curly brackets were never allowed in namespace URIs
+according to RFC 3986, and so-called namespace-validating
+XML parsers have the right to reject them a invalid URIs.
+
+libexpat >=2.4.5 has become strcter in that regard due to
+related security issues; with ET.XML instantiating a
+namespace-aware parser under the hood, this test has no
+future in CPython.
+
+References:
+- https://datatracker.ietf.org/doc/html/rfc3968
+- https://www.w3.org/TR/xml-names/
+
+Also, test_minidom.py: Support Expat >=2.4.5
+
+(rebased for 2.7.18 by Michał Górny)
+---
+ Lib/test/test_minidom.py | 12 ++++++++++--
+ Lib/test/test_xml_etree.py | 6 ------
+ 2 files changed, 10 insertions(+), 8 deletions(-)
+
+diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py
+index 2eb642395b..3aa524c365 100644
+--- a/Lib/test/test_minidom.py
++++ b/Lib/test/test_minidom.py
+@@ -6,12 +6,14 @@ from StringIO import StringIO
+ from test import support
+ import unittest
+
++import pyexpat
+ import xml.dom
+ import xml.dom.minidom
+ import xml.parsers.expat
+
+ from xml.dom.minidom import parse, Node, Document, parseString
+ from xml.dom.minidom import getDOMImplementation
++from xml.parsers.expat import ExpatError
+
+
+ tstfile = support.findfile("test.xml", subdir="xmltestdata")
+@@ -1051,8 +1053,14 @@ class MinidomTest(unittest.TestCase):
+
+ # Verify that character decoding errors raise exceptions instead
+ # of crashing
+- self.assertRaises(UnicodeDecodeError, parseString,
+- '<fran\xe7ais>Comment \xe7a va ? Tr\xe8s bien ?</fran\xe7ais>')
++ if pyexpat.version_info >= (2, 4, 5):
++ self.assertRaises(ExpatError, parseString,
++ b'<fran\xe7ais></fran\xe7ais>')
++ self.assertRaises(ExpatError, parseString,
++ b'<franais>Comment \xe7a va ? Tr\xe8s bien ?</franais>')
++ else:
++ self.assertRaises(UnicodeDecodeError, parseString,
++ b'<fran\xe7ais>Comment \xe7a va ? Tr\xe8s bien ?</fran\xe7ais>')
+
+ doc.unlink()
+
+diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
+index c75d55f05c..0855bc07ca 100644
+--- a/Lib/test/test_xml_etree.py
++++ b/Lib/test/test_xml_etree.py
+@@ -1482,12 +1482,6 @@ class BugsTest(unittest.TestCase):
+ b"<?xml version='1.0' encoding='ascii'?>\n"
+ b'<body>tãg</body>')
+
+- def test_issue3151(self):
+- e = ET.XML('<prefix:localname xmlns:prefix="${stuff}"/>')
+- self.assertEqual(e.tag, '{${stuff}}localname')
+- t = ET.ElementTree(e)
+- self.assertEqual(ET.tostring(e), b'<ns0:localname xmlns:ns0="${stuff}" />')
+-
+ def test_issue6565(self):
+ elem = ET.XML("<body><tag/></body>")
+ self.assertEqual(summarize_list(elem), ['tag'])
+--
+2.38.1
+
diff --git a/python-test-support-unlink-errno.patch b/python-test-support-unlink-errno.patch
new file mode 100644
index 0000000..aeca9d2
--- /dev/null
+++ b/python-test-support-unlink-errno.patch
@@ -0,0 +1,21 @@
+From: Mike Gilbert <floppym at gentoo.org>
+Subject: test.support.unlink: ignore EACCES and EROFS
+
+test.support.forget() removes a module's .pyc/.pyo from every sys.path entry,
+including ones the build environment mounts read-only (/usr/local/lib*) - there
+can be no stale .pyc there, so the failure to remove one is not a test failure.
+
+Based on Gentoo's https://bugs.gentoo.org/679628 (EACCES); PLD's builder mounts
+the tree read-only rather than denying the write, so EROFS is needed too.
+
+--- a/Lib/test/support/__init__.py
++++ b/Lib/test/support/__init__.py
+@@ -291,7 +291,7 @@ def unlink(filename):
+ try:
+ _unlink(filename)
+ except OSError as exc:
+- if exc.errno not in (errno.ENOENT, errno.ENOTDIR):
++ if exc.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EACCES, errno.EROFS):
+ raise
+
+ def rmdir(dirname):
================================================================
---- gitweb:
http://git.pld-linux.org/gitweb.cgi/packages/python.git/commitdiff/d265a4e262afa1319ff48749f6dd0d4a06fdfb2e
More information about the pld-cvs-commit
mailing list