diff --git a/SOURCES/CVE-2018-18074.patch b/SOURCES/CVE-2018-18074.patch new file mode 100644 index 0000000..21a9e7f --- /dev/null +++ b/SOURCES/CVE-2018-18074.patch @@ -0,0 +1,90 @@ +From ffbfdb53681207b23bcf67dd76368ad6185ade24 Mon Sep 17 00:00:00 2001 +From: Lumir Balhar +Date: Thu, 16 Jan 2020 07:06:09 +0100 +Subject: [PATCH] Fix for CVE-2018-18074 + +This patch contains the fix for CVE-2018-18074 and +a subsequent regression fix combined in one. +--- + sessions.py | 36 +++++++++++++++++++++++++++++------- + utils.py | 1 + + 2 files changed, 30 insertions(+), 7 deletions(-) + +diff --git a/sessions.py b/sessions.py +index 6570e73..4038047 100644 +--- a/sessions.py ++++ b/sessions.py +@@ -29,7 +29,7 @@ from .adapters import HTTPAdapter + + from .utils import ( + requote_uri, get_environ_proxies, get_netrc_auth, should_bypass_proxies, +- get_auth_from_url, rewind_body ++ get_auth_from_url, rewind_body, DEFAULT_PORTS + ) + + from .status_codes import codes +@@ -116,6 +116,32 @@ class SessionRedirectMixin(object): + return to_native_string(location, 'utf8') + return None + ++ ++ def should_strip_auth(self, old_url, new_url): ++ """Decide whether Authorization header should be removed when redirecting""" ++ old_parsed = urlparse(old_url) ++ new_parsed = urlparse(new_url) ++ if old_parsed.hostname != new_parsed.hostname: ++ return True ++ # Special case: allow http -> https redirect when using the standard ++ # ports. This isn't specified by RFC 7235, but is kept to avoid ++ # breaking backwards compatibility with older versions of requests ++ # that allowed any redirects on the same host. ++ if (old_parsed.scheme == 'http' and old_parsed.port in (80, None) ++ and new_parsed.scheme == 'https' and new_parsed.port in (443, None)): ++ return False ++ ++ # Handle default port usage corresponding to scheme. ++ changed_port = old_parsed.port != new_parsed.port ++ changed_scheme = old_parsed.scheme != new_parsed.scheme ++ default_port = (DEFAULT_PORTS.get(old_parsed.scheme, None), None) ++ if (not changed_scheme and old_parsed.port in default_port ++ and new_parsed.port in default_port): ++ return False ++ ++ # Standard case: root URI must match ++ return changed_port or changed_scheme ++ + def resolve_redirects(self, resp, req, stream=False, timeout=None, + verify=True, cert=None, proxies=None, yield_requests=False, **adapter_kwargs): + """Receives a Response. Returns a generator of Responses or Requests.""" +@@ -232,14 +258,10 @@ class SessionRedirectMixin(object): + headers = prepared_request.headers + url = prepared_request.url + +- if 'Authorization' in headers: ++ if 'Authorization' in headers and self.should_strip_auth(response.request.url, url): + # If we get redirected to a new host, we should strip out any + # authentication headers. +- original_parsed = urlparse(response.request.url) +- redirect_parsed = urlparse(url) +- +- if (original_parsed.hostname != redirect_parsed.hostname): +- del headers['Authorization'] ++ del headers['Authorization'] + + # .netrc might have more auth for us on our new host. + new_auth = get_netrc_auth(url) if self.trust_env else None +diff --git a/utils.py b/utils.py +index 5c47de9..5695ab0 100644 +--- a/utils.py ++++ b/utils.py +@@ -38,6 +38,7 @@ NETRC_FILES = ('.netrc', '_netrc') + + DEFAULT_CA_BUNDLE_PATH = certs.where() + ++DEFAULT_PORTS = {'http': 80, 'https': 443} + + if platform.system() == 'Windows': + # provide a proxy_bypass version on Windows without DNS lookups +-- +2.24.1 + diff --git a/SOURCES/CVE-2018-20060.patch b/SOURCES/CVE-2018-20060.patch new file mode 100644 index 0000000..cc3b741 --- /dev/null +++ b/SOURCES/CVE-2018-20060.patch @@ -0,0 +1,94 @@ +From c734f873270cf9ca414832423f7aad98443c379f Mon Sep 17 00:00:00 2001 +From: Lumir Balhar +Date: Thu, 9 Jan 2020 11:26:24 +0100 +Subject: [PATCH] CVE-2018-20060 + +--- + poolmanager.py | 11 ++++++++++- + util/retry.py | 12 +++++++++++- + 2 files changed, 21 insertions(+), 2 deletions(-) + +diff --git a/poolmanager.py b/poolmanager.py +index 4ae9174..bfa5115 100644 +--- a/poolmanager.py ++++ b/poolmanager.py +@@ -312,8 +312,9 @@ class PoolManager(RequestMethods): + + kw['assert_same_host'] = False + kw['redirect'] = False ++ + if 'headers' not in kw: +- kw['headers'] = self.headers ++ kw['headers'] = self.headers.copy() + + if self.proxy is not None and u.scheme == "http": + response = conn.urlopen(method, url, **kw) +@@ -335,6 +336,14 @@ class PoolManager(RequestMethods): + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect) + ++ # Strip headers marked as unsafe to forward to the redirected location. ++ # Check remove_headers_on_redirect to avoid a potential network call within ++ # conn.is_same_host() which may use socket.gethostbyname() in the future. ++ if (retries.remove_headers_on_redirect ++ and not conn.is_same_host(redirect_location)): ++ for header in retries.remove_headers_on_redirect: ++ kw['headers'].pop(header, None) ++ + try: + retries = retries.increment(method, url, response=response, _pool=conn) + except MaxRetryError: +diff --git a/util/retry.py b/util/retry.py +index c603cb4..0b83963 100644 +--- a/util/retry.py ++++ b/util/retry.py +@@ -126,6 +126,11 @@ class Retry(object): + exhausted, to raise a MaxRetryError, or to return a response with a + response code in the 3xx range. + ++ :param iterable remove_headers_on_redirect: ++ Sequence of headers to remove from the request when a response ++ indicating a redirect is returned before firing off the redirected ++ request ++ + :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: + whether we should raise an exception, or return a response, + if status falls in ``status_forcelist`` range and retries have +@@ -144,6 +149,8 @@ class Retry(object): + DEFAULT_METHOD_WHITELIST = frozenset([ + 'HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']) + ++ DEFAULT_REDIRECT_HEADERS_BLACKLIST = frozenset(['Authorization']) ++ + RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) + + #: Maximum backoff time. +@@ -152,7 +159,8 @@ class Retry(object): + def __init__(self, total=10, connect=None, read=None, redirect=None, status=None, + method_whitelist=DEFAULT_METHOD_WHITELIST, status_forcelist=None, + backoff_factor=0, raise_on_redirect=True, raise_on_status=True, +- history=None, respect_retry_after_header=True): ++ history=None, respect_retry_after_header=True, ++ remove_headers_on_redirect=DEFAULT_REDIRECT_HEADERS_BLACKLIST): + + self.total = total + self.connect = connect +@@ -171,6 +179,7 @@ class Retry(object): + self.raise_on_status = raise_on_status + self.history = history or tuple() + self.respect_retry_after_header = respect_retry_after_header ++ self.remove_headers_on_redirect = remove_headers_on_redirect + + def new(self, **kw): + params = dict( +@@ -182,6 +191,7 @@ class Retry(object): + raise_on_redirect=self.raise_on_redirect, + raise_on_status=self.raise_on_status, + history=self.history, ++ remove_headers_on_redirect=self.remove_headers_on_redirect, + ) + params.update(kw) + return type(self)(**params) +-- +2.24.1 + diff --git a/SOURCES/CVE-2019-11236.patch b/SOURCES/CVE-2019-11236.patch new file mode 100644 index 0000000..f8729d5 --- /dev/null +++ b/SOURCES/CVE-2019-11236.patch @@ -0,0 +1,43 @@ +From b40eb0f43daecc6e2e3ce47b0be49cf570d02adc Mon Sep 17 00:00:00 2001 +From: Lumir Balhar +Date: Thu, 9 Jan 2020 11:14:58 +0100 +Subject: [PATCH] CVE-2019-9740 + +--- + util/url.py | 7 +++++++ + 1 file changed, 7 insertions(+) + +diff --git a/util/url.py b/util/url.py +index 6b6f996..2784c85 100644 +--- a/util/url.py ++++ b/util/url.py +@@ -1,5 +1,6 @@ + from __future__ import absolute_import + from collections import namedtuple ++import re + + from ..exceptions import LocationParseError + +@@ -10,6 +11,8 @@ url_attrs = ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'] + # urllib3 infers URLs without a scheme (None) to be http. + NORMALIZABLE_SCHEMES = ('http', 'https', None) + ++_contains_disallowed_url_pchar_re = re.compile('[\x00-\x20\x7f]') ++from ..packages.six.moves.urllib.parse import quote + + class Url(namedtuple('Url', url_attrs)): + """ +@@ -155,6 +158,10 @@ def parse_url(url): + # Empty + return Url() + ++ # Prevent CVE-2019-9740. ++ # adapted from https://github.com/python/cpython/pull/12755 ++ url = _contains_disallowed_url_pchar_re.sub(lambda match: quote(match.group()), url) ++ + scheme = None + auth = None + host = None +-- +2.24.1 + diff --git a/SOURCES/CVE-2019-11324.patch b/SOURCES/CVE-2019-11324.patch new file mode 100644 index 0000000..b1016ab --- /dev/null +++ b/SOURCES/CVE-2019-11324.patch @@ -0,0 +1,26 @@ +From 54e768a6dbe3cadeb456dea37bbeaf6e1e17e87c Mon Sep 17 00:00:00 2001 +From: Lumir Balhar +Date: Thu, 9 Jan 2020 10:47:27 +0100 +Subject: [PATCH] CVE-2019-11324 Certification mishandle when error should be + thrown + +--- + util/ssl_.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/util/ssl_.py b/util/ssl_.py +index 32fd9ed..f9f12ff 100644 +--- a/util/ssl_.py ++++ b/util/ssl_.py +@@ -319,7 +319,7 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, + if e.errno == errno.ENOENT: + raise SSLError(e) + raise +- elif getattr(context, 'load_default_certs', None) is not None: ++ elif ssl_context is None and hasattr(context, 'load_default_certs'): + # try to load OS default certs; works well on Windows (require Python3.4+) + context.load_default_certs() + +-- +2.24.1 + diff --git a/SPECS/python-pip.spec b/SPECS/python-pip.spec index ef6d630..5c4829c 100644 --- a/SPECS/python-pip.spec +++ b/SPECS/python-pip.spec @@ -19,7 +19,7 @@ Name: python-%{srcname} # When updating, update the bundled libraries versions bellow! Version: 9.0.3 -Release: 15%{?dist} +Release: 17%{?dist} Summary: A tool for installing and managing Python packages Group: Development/Libraries @@ -90,6 +90,29 @@ Patch3: pip-nowarn-upgrade.patch # https://bugzilla.redhat.com/show_bug.cgi?id=1655255 Patch4: dummy-certifi.patch +# Patch for CVE in the bundled urllib3 +# CVE-2018-20060 Cross-host redirect does not remove Authorization header allow for credential exposure +# https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2018-20060 +Patch5: CVE-2018-20060.patch + +# Patch for CVE in the bundled urllib3 +# CVE-2019-11236 CRLF injection due to not encoding the '\r\n' sequence leading to possible attack on internal service +# https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2019-11236 +Patch6: CVE-2019-11236.patch + +# Patch for CVE in the bundled urllib3 +# CVE-2019-11324 Certification mishandle when error should be thrown +# https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2019-11324 +Patch7: CVE-2019-11324.patch + +# Patch for CVE in the bundled requests +# CVE-2018-18074 Redirect from HTTPS to HTTP does not remove Authorization header +# This patch fixes both the CVE +# https://bugzilla.redhat.com/show_bug.cgi?id=1643829 +# and the subsequent regression +# https://github.com/psf/requests/pull/4851 +Patch8: CVE-2018-18074.patch + %global _description \ pip is a package management system used to install and manage software packages \ written in Python. Many packages can be found in the Python Package Index \ @@ -218,8 +241,19 @@ tar -xf %{SOURCE1} %patch3 -p1 %patch4 -p1 +# Patching of bundled libraries +pushd pip/_vendor/urllib3 +%patch5 -p1 +%patch6 -p1 +%patch7 -p1 +popd +pushd pip/_vendor/requests +%patch8 -p1 +popd + # this goes together with patch4 rm pip/_vendor/certifi/*.pem +rm pip/_vendor/requests/*.pem sed -i '/\.pem$/d' pip.egg-info/SOURCES.txt sed -i '1d' pip/__init__.py @@ -338,6 +372,18 @@ py.test-%{python3_version} -m 'not network' %endif %changelog +* Wed Mar 04 2020 Charalampos Stratakis - 9.0.3-17 +- Remove unused CA bundle from the bundled requests library +Resolves: rhbz#1775200 + +* Mon Jan 13 2020 Lumír Balhar - 9.0.3-16 +- Add four new patches for CVEs in bundled urllib3 and requests +CVE-2018-20060, CVE-2019-11236, CVE-2019-11324, CVE-2018-18074 +Resolves: rhbz#1649153 +Resolves: rhbz#1700824 +Resolves: rhbz#1702473 +Resolves: rhbz#1643829 + * Thu Jun 06 2019 Charalampos Stratakis - 9.0.3-15 - Create python-pip-wheel package with the wheel Resolves: rhbz#1718031