Blame SOURCES/pip-directory-traversal-security-issue.patch

bd5b0e
From 8044d9f2fbcb09f09a62b26ac1d8a134976bb2ac Mon Sep 17 00:00:00 2001
bd5b0e
From: gzpan123 <gzpan123@gmail.com>
bd5b0e
Date: Wed, 17 Apr 2019 21:25:45 +0800
bd5b0e
Subject: [PATCH 1/2] FIX #6413 pip install <url> allow directory traversal
bd5b0e
bd5b0e
---
bd5b0e
 news/6413.bugfix |  3 +++
bd5b0e
 pip/download.py  | 31 ++++++++++++++++++++++++++-----
bd5b0e
 2 files changed, 29 insertions(+), 5 deletions(-)
bd5b0e
 create mode 100644 news/6413.bugfix
bd5b0e
bd5b0e
diff --git a/news/6413.bugfix b/news/6413.bugfix
bd5b0e
new file mode 100644
bd5b0e
index 0000000..68d0a72
bd5b0e
--- /dev/null
bd5b0e
+++ b/news/6413.bugfix
bd5b0e
@@ -0,0 +1,3 @@
bd5b0e
+Prevent ``pip install <url>`` from permitting directory traversal if e.g.
bd5b0e
+a malicious server sends a ``Content-Disposition`` header with a filename
bd5b0e
+containing ``../`` or ``..\\``.
bd5b0e
diff --git a/pip/download.py b/pip/download.py
bd5b0e
index 039e55a..b3d169b 100644
bd5b0e
--- a/pip/download.py
bd5b0e
+++ b/pip/download.py
bd5b0e
@@ -54,7 +54,8 @@ __all__ = ['get_file_content',
bd5b0e
            'is_url', 'url_to_path', 'path_to_url',
bd5b0e
            'is_archive_file', 'unpack_vcs_link',
bd5b0e
            'unpack_file_url', 'is_vcs_url', 'is_file_url',
bd5b0e
-           'unpack_http_url', 'unpack_url']
bd5b0e
+           'unpack_http_url', 'unpack_url',
bd5b0e
+           'parse_content_disposition', 'sanitize_content_filename']
bd5b0e
 
bd5b0e
 
bd5b0e
 logger = logging.getLogger(__name__)
bd5b0e
@@ -824,6 +825,29 @@ def unpack_url(link, location, download_dir=None,
bd5b0e
         write_delete_marker_file(location)
bd5b0e
 
bd5b0e
 
bd5b0e
+def sanitize_content_filename(filename):
bd5b0e
+    # type: (str) -> str
bd5b0e
+    """
bd5b0e
+    Sanitize the "filename" value from a Content-Disposition header.
bd5b0e
+    """
bd5b0e
+    return os.path.basename(filename)
bd5b0e
+
bd5b0e
+
bd5b0e
+def parse_content_disposition(content_disposition, default_filename):
bd5b0e
+    # type: (str, str) -> str
bd5b0e
+    """
bd5b0e
+    Parse the "filename" value from a Content-Disposition header, and
bd5b0e
+    return the default filename if the result is empty.
bd5b0e
+    """
bd5b0e
+    _type, params = cgi.parse_header(content_disposition)
bd5b0e
+    filename = params.get('filename')
bd5b0e
+    if filename:
bd5b0e
+        # We need to sanitize the filename to prevent directory traversal
bd5b0e
+        # in case the filename contains ".." path parts.
bd5b0e
+        filename = sanitize_content_filename(filename)
bd5b0e
+    return filename or default_filename
bd5b0e
+
bd5b0e
+
bd5b0e
 def _download_http_url(link, session, temp_dir, hashes):
bd5b0e
     """Download link url into temp_dir using provided session"""
bd5b0e
     target_url = link.url.split('#', 1)[0]
bd5b0e
@@ -864,10 +888,7 @@ def _download_http_url(link, session, temp_dir, hashes):
bd5b0e
     # Have a look at the Content-Disposition header for a better guess
bd5b0e
     content_disposition = resp.headers.get('content-disposition')
bd5b0e
     if content_disposition:
bd5b0e
-        type, params = cgi.parse_header(content_disposition)
bd5b0e
-        # We use ``or`` here because we don't want to use an "empty" value
bd5b0e
-        # from the filename param.
bd5b0e
-        filename = params.get('filename') or filename
bd5b0e
+        filename = parse_content_disposition(content_disposition, filename)
bd5b0e
     ext = splitext(filename)[1]
bd5b0e
     if not ext:
bd5b0e
         ext = mimetypes.guess_extension(content_type)
bd5b0e
-- 
bd5b0e
2.25.4
bd5b0e