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