Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
From 138e2caeb4827ccfd1eaff2cf63afb79dfeeb3c4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= <mgorny@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@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

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
From 6a6c4240fa1e628dbcca09fdde39aea4d8eb6138 Mon Sep 17 00:00:00 2001
From: "Miss Skeleton (bot)" <31488909+miss-islington@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@gmail.com>

Rebased for Python 2.7 by Michał Górny <mgorny@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

213 changes: 213 additions & 0 deletions pkgs/development/interpreters/python/cpython/2.7/CVE-2020-8492.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
From 2273e65e11dd0234f2f51ebaef61fc6e848d4059 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= <mgorny@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@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@example.com"
+ realm2 = "realm2@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

7 changes: 4 additions & 3 deletions pkgs/development/interpreters/python/cpython/2.7/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,12 @@ let
# Backport from CPython 3.8 of a good list of tests to run for PGO.
./profile-task.patch

# Patch is likely to go away in the next release (if there is any)
# https://www.activestate.com/products/python/python-2-end-of-life-security-updates/
./CVE-2019-20907.patch

./CVE-2020-8492.patch
./CVE-2020-26116.patch
./CVE-2020-27619.patch
./CVE-2021-3177.patch

./CVE-2021-23336.patch

# The workaround is for unittests on Win64, which we don't support.
Expand Down