VYPR
High severityNVD Advisory· Published Sep 4, 2020· Updated Aug 5, 2024

CVE-2019-20916

CVE-2019-20916

Description

The pip package before 19.2 for Python allows Directory Traversal when a URL is given in an install command, because a Content-Disposition header can have ../ in a filename, as demonstrated by overwriting the /root/.ssh/authorized_keys file. This occurs in _download_http_url in _internal/download.py.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Pip before 19.2 allows directory traversal via malicious Content-Disposition header in URL, enabling arbitrary file overwrite.

The vulnerability exists in pip's _download_http_url function in _internal/download.py. When installing a package from a URL, the filename in the Content-Disposition header is not properly sanitized, allowing path traversal sequences like ../ [1]. This was fixed in pip version 19.2, which introduced the sanitize_content_filename function to strip dangerous path components [4].

Exploitation requires supplying a crafted URL whose server responds with a Content-Disposition header containing ../ in the filename. No special privileges are needed; the victim simply runs pip install with the malicious URL [2]. The GitHub advisory (GHSA-gpvv-69j7-gwj8) highlights the simplicity of the attack [2].

An attacker can overwrite arbitrary files on the target system. A commonly cited example is overwriting /root/.ssh/authorized_keys to add an SSH key, thereby gaining remote access [1]. The impact is limited only by the permissions of the user running pip.

The vulnerability is fixed in pip 19.2 and later. Users are advised to upgrade immediately. No workarounds are available other than not installing packages from untrusted URLs [3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
pipPyPI
< 19.219.2

Affected products

268

Patches

2
0e642958ada5

Generate NEWS

https://github.com/pypa/pipPradyun GedamJul 22, 2019via osv
57 files changed · +85 70
  • news/1234.trivial+0 0 removed
  • news/1890.bugfix+0 2 removed
    @@ -1,2 +0,0 @@
    -Set ``sys.argv[0]`` to the underlying ``setup.py`` when invoking ``setup.py``
    -via the setuptools shim so setuptools doesn't think the path is ``-c``.
    
  • news/3662.trivial+0 1 removed
    @@ -1 +0,0 @@
    -Work around an issue with Jython's `re` implementation that resulted in a Java StackOverflowError.
    
  • news/3799.feature+0 1 removed
    @@ -1 +0,0 @@
    -Fully support using ``--trusted-host`` inside requirements files.
    
  • news/4733.doc+0 1 removed
    @@ -1 +0,0 @@
    -Replace a failing example of pip installs with extras with a working one.
    
  • news/5059.trivial+0 0 removed
  • news/5082.feature+0 2 removed
    @@ -1,2 +0,0 @@
    -Improve the error message when ``METADATA`` or ``PKG-INFO`` is None when
    -accessing metadata.
    
  • news/5169.feature+0 1 removed
    @@ -1 +0,0 @@
    -Display hint on installing with --pre when search results include pre-release versions.
    \ No newline at end of file
    
  • news/5369.bugfix+0 2 removed
    @@ -1,2 +0,0 @@
    -Update ``pip download`` to respect the given ``--python-version`` when checking
    -``"Requires-Python"``.
    
  • news/5499.feature+0 1 removed
    @@ -1 +0,0 @@
    -Report to Warehouse that pip is running under CI if the ``PIP_IS_CI`` environment variable is set.
    
  • news/5518.bugfix+0 2 removed
    @@ -1,2 +0,0 @@
    -Respect ``--global-option`` and ``--install-option`` when installing from
    -a version control url (e.g. ``git``).
    
  • news/5671.bugfix+0 1 removed
    @@ -1 +0,0 @@
    -Make the "ascii" progress bar really be "ascii" and not Unicode.
    
  • news/5874.feature+0 2 removed
    @@ -1,2 +0,0 @@
    -When choosing candidates to install, prefer candidates with a hash matching
    -one of the user-provided hashes.
    
  • news/5908.feature+0 2 removed
    @@ -1,2 +0,0 @@
    -Log the final filename and SHA256 of a ``.whl`` file when done building a
    -wheel.
    
  • news/5948.feature+0 1 removed
    @@ -1 +0,0 @@
    -Credentials will now be loaded using `keyring` when installed.
    
  • news/5963.bugfix+0 1 removed
    @@ -1 +0,0 @@
    -Fail elegantly when trying to set an incorrectly formatted key in config.
    
  • news/6008.bugfix+0 1 removed
    @@ -1 +0,0 @@
    -Prevent DistutilsOptionError when prefix is indicated in the global environment and `--target` is used.
    
  • news/6121.feature+0 2 removed
    @@ -1,2 +0,0 @@
    -Include the wheel's tags in the log message explanation when a candidate
    -wheel link is found incompatible.
    
  • news/6371.bugfix+0 2 removed
    @@ -1,2 +0,0 @@
    -Fix ``pip install`` to respect ``--ignore-requires-python`` when evaluating
    -links.
    
  • news/6383.bugfix+0 2 removed
    @@ -1,2 +0,0 @@
    -Fix a debug log message when freezing an editable, non-version controlled
    -requirement.
    
  • news/6386.bugfix+0 2 removed
    @@ -1,2 +0,0 @@
    -Extend to Subversion 1.8+ the behavior of calling Subversion in
    -interactive mode when pip is run interactively.
    
  • news/6404.feature+0 2 removed
    @@ -1,2 +0,0 @@
    -Add a ``--path`` argument to ``pip freeze`` to support ``--target``
    -installations.
    
  • news/6413.bugfix+0 3 removed
    @@ -1,3 +0,0 @@
    -Prevent ``pip install <url>`` from permitting directory traversal if e.g.
    -a malicious server sends a ``Content-Disposition`` header with a filename
    -containing ``../`` or ``..\\``.
    
  • news/6471.doc+0 1 removed
    @@ -1 +0,0 @@
    -Upgrade Sphinx version used to build documentation.
    
  • news/6486.trivial+0 1 removed
    @@ -1 +0,0 @@
    -This change will add .DS_Store to .gitignore
    
  • news/6489.bugfix+0 1 removed
    @@ -1 +0,0 @@
    -Hide passwords in output when using ``--find-links``.
    
  • news/6512.doc+0 1 removed
    @@ -1 +0,0 @@
    -Mention that pip can install from git refs.
    
  • news/6513.bugfix+0 2 removed
    @@ -1,2 +0,0 @@
    -Include more details in the log message if ``pip freeze`` can't generate a
    -requirement string for a particular distribution.
    
  • news/6527.bugfix+0 2 removed
    @@ -1,2 +0,0 @@
    -Add the line number and file location to the error message when reading an
    -invalid requirements file in certain situations.
    
  • news/6533.trivial+0 1 removed
    @@ -1 +0,0 @@
    -Override the definition of the function was_installed_by_pip (src\pip\_internal\utils\outdated.py) too specific with a more general alternative
    \ No newline at end of file
    
  • news/6543.bugfix+0 1 removed
    @@ -1 +0,0 @@
    -Prefer ``os.confstr`` to ``ctypes`` when extracting glibc version info.
    
  • news/6549.feature+0 1 removed
    @@ -1 +0,0 @@
    -Improve deprecation messages to include the version in which the functionality will be removed.
    
  • news/6551.feature+0 2 removed
    @@ -1,2 +0,0 @@
    -Add a ``--path`` argument to ``pip list`` to support ``--target``
    -installations.
    
  • news/6579.trivial+0 1 removed
    @@ -1 +0,0 @@
    -Link with developer documentation added in .github/CONTRIBUTING.md
    \ No newline at end of file
    
  • news/6585.feature+0 2 removed
    @@ -1,2 +0,0 @@
    -Allow ``--python-version`` to be passed as a dotted version string (e.g.
    -``3.7`` or ``3.7.3``).
    
  • news/6587.feature+0 1 removed
    @@ -1 +0,0 @@
    -Update timestamps in pip's ``--log`` file to include milliseconds.
    
  • news/6633.feature+0 2 removed
    @@ -1,2 +0,0 @@
    -Respect whether a file has been marked as "yanked" from a simple repository
    -(see `PEP 592 <https://www.python.org/dev/peps/pep-0592/>`__ for details).
    
  • news/6638.feature+0 2 removed
    @@ -1,2 +0,0 @@
    -Add a new command ``pip debug`` that can display e.g. the list of compatible
    -tags for the current Python.
    
  • news/6644.trivial+0 0 removed
  • news/6648.bugfix+0 1 removed
    @@ -1 +0,0 @@
    -Improve error message printed when an invalid editable requirement is provided.
    
  • news/6651.bugfix+0 1 removed
    @@ -1 +0,0 @@
    -Improve error message formatting when a command errors out in a subprocess.
    
  • news/6659.trivial+0 0 removed
  • news/6675.bugfix+0 1 removed
    @@ -1 +0,0 @@
    -Prefer ``os.confstr`` to ``ctypes`` when extracting glibc version info.
    
  • news/6685.removal+0 1 removed
    @@ -1 +0,0 @@
    -Drop support for EOL Python 3.4.
    
  • news/6724.doc+0 1 removed
    @@ -1 +0,0 @@
    -Fix generation of subcommand manpages.
    
  • news/6726.doc+0 1 removed
    @@ -1 +0,0 @@
    -Document how Python 2.7 support will be maintained.
    
  • news/certifi.vendor+0 1 removed
    @@ -1 +0,0 @@
    -Upgrade certifi to 2019.6.16
    
  • news/cleanup.trivial+0 2 removed
    @@ -1,2 +0,0 @@
    -Exit conditional sooner if not local_version_is_older for faster execution (src/pip/_internal/utils/outdated.py)
    -Moved local and remote pip version check conditional to a variable (src/pip/_internal/utils/outdated.py)
    \ No newline at end of file
    
  • news/distlib.vendor+0 1 removed
    @@ -1 +0,0 @@
    -Upgrade distlib to 0.2.9.post0
    
  • news/git_looks_like_hash.trivial+0 1 removed
    @@ -1 +0,0 @@
    -Be stricter in identifying git commit hashes.
    \ No newline at end of file
    
  • news/html5lib-collections-patch.vendor+0 1 removed
    @@ -1 +0,0 @@
    -Patch vendored html5lib, to prefer using `collections.abc` where possible.
    
  • news/msgpack.vendor+0 1 removed
    @@ -1 +0,0 @@
    -Upgrade msgpack to 0.6.1
    
  • news/pathlib-refactor-1.trivial+0 0 removed
  • news/pathlib-refactor-2.trivial+0 0 removed
  • news/requests.vendor+0 1 removed
    @@ -1 +0,0 @@
    -Upgrade requests to 2.22.0
    
  • NEWS.rst+85 0 modified
    @@ -7,6 +7,91 @@
     
     .. towncrier release notes start
     
    +19.2 (2019-07-22)
    +=================
    +
    +Deprecations and Removals
    +-------------------------
    +
    +- Drop support for EOL Python 3.4. (`#6685 <https://github.com/pypa/pip/issues/6685>`_)
    +- Improve deprecation messages to include the version in which the functionality will be removed. (`#6549 <https://github.com/pypa/pip/issues/6549>`_)
    +
    +Features
    +--------
    +
    +- Credentials will now be loaded using `keyring` when installed. (`#5948 <https://github.com/pypa/pip/issues/5948>`_)
    +- Fully support using ``--trusted-host`` inside requirements files. (`#3799 <https://github.com/pypa/pip/issues/3799>`_)
    +- Update timestamps in pip's ``--log`` file to include milliseconds. (`#6587 <https://github.com/pypa/pip/issues/6587>`_)
    +- Respect whether a file has been marked as "yanked" from a simple repository
    +  (see `PEP 592 <https://www.python.org/dev/peps/pep-0592/>`__ for details). (`#6633 <https://github.com/pypa/pip/issues/6633>`_)
    +- When choosing candidates to install, prefer candidates with a hash matching
    +  one of the user-provided hashes. (`#5874 <https://github.com/pypa/pip/issues/5874>`_)
    +- Improve the error message when ``METADATA`` or ``PKG-INFO`` is None when
    +  accessing metadata. (`#5082 <https://github.com/pypa/pip/issues/5082>`_)
    +- Add a new command ``pip debug`` that can display e.g. the list of compatible
    +  tags for the current Python. (`#6638 <https://github.com/pypa/pip/issues/6638>`_)
    +- Display hint on installing with --pre when search results include pre-release versions. (`#5169 <https://github.com/pypa/pip/issues/5169>`_)
    +- Report to Warehouse that pip is running under CI if the ``PIP_IS_CI`` environment variable is set. (`#5499 <https://github.com/pypa/pip/issues/5499>`_)
    +- Allow ``--python-version`` to be passed as a dotted version string (e.g.
    +  ``3.7`` or ``3.7.3``). (`#6585 <https://github.com/pypa/pip/issues/6585>`_)
    +- Log the final filename and SHA256 of a ``.whl`` file when done building a
    +  wheel. (`#5908 <https://github.com/pypa/pip/issues/5908>`_)
    +- Include the wheel's tags in the log message explanation when a candidate
    +  wheel link is found incompatible. (`#6121 <https://github.com/pypa/pip/issues/6121>`_)
    +- Add a ``--path`` argument to ``pip freeze`` to support ``--target``
    +  installations. (`#6404 <https://github.com/pypa/pip/issues/6404>`_)
    +- Add a ``--path`` argument to ``pip list`` to support ``--target``
    +  installations. (`#6551 <https://github.com/pypa/pip/issues/6551>`_)
    +
    +Bug Fixes
    +---------
    +
    +- Set ``sys.argv[0]`` to the underlying ``setup.py`` when invoking ``setup.py``
    +  via the setuptools shim so setuptools doesn't think the path is ``-c``. (`#1890 <https://github.com/pypa/pip/issues/1890>`_)
    +- Update ``pip download`` to respect the given ``--python-version`` when checking
    +  ``"Requires-Python"``. (`#5369 <https://github.com/pypa/pip/issues/5369>`_)
    +- Respect ``--global-option`` and ``--install-option`` when installing from
    +  a version control url (e.g. ``git``). (`#5518 <https://github.com/pypa/pip/issues/5518>`_)
    +- Make the "ascii" progress bar really be "ascii" and not Unicode. (`#5671 <https://github.com/pypa/pip/issues/5671>`_)
    +- Fail elegantly when trying to set an incorrectly formatted key in config. (`#5963 <https://github.com/pypa/pip/issues/5963>`_)
    +- Prevent DistutilsOptionError when prefix is indicated in the global environment and `--target` is used. (`#6008 <https://github.com/pypa/pip/issues/6008>`_)
    +- Fix ``pip install`` to respect ``--ignore-requires-python`` when evaluating
    +  links. (`#6371 <https://github.com/pypa/pip/issues/6371>`_)
    +- Fix a debug log message when freezing an editable, non-version controlled
    +  requirement. (`#6383 <https://github.com/pypa/pip/issues/6383>`_)
    +- Extend to Subversion 1.8+ the behavior of calling Subversion in
    +  interactive mode when pip is run interactively. (`#6386 <https://github.com/pypa/pip/issues/6386>`_)
    +- Prevent ``pip install <url>`` from permitting directory traversal if e.g.
    +  a malicious server sends a ``Content-Disposition`` header with a filename
    +  containing ``../`` or ``..\\``. (`#6413 <https://github.com/pypa/pip/issues/6413>`_)
    +- Hide passwords in output when using ``--find-links``. (`#6489 <https://github.com/pypa/pip/issues/6489>`_)
    +- Include more details in the log message if ``pip freeze`` can't generate a
    +  requirement string for a particular distribution. (`#6513 <https://github.com/pypa/pip/issues/6513>`_)
    +- Add the line number and file location to the error message when reading an
    +  invalid requirements file in certain situations. (`#6527 <https://github.com/pypa/pip/issues/6527>`_)
    +- Prefer ``os.confstr`` to ``ctypes`` when extracting glibc version info. (`#6543 <https://github.com/pypa/pip/issues/6543>`_, `#6675 <https://github.com/pypa/pip/issues/6675>`_)
    +- Improve error message printed when an invalid editable requirement is provided. (`#6648 <https://github.com/pypa/pip/issues/6648>`_)
    +- Improve error message formatting when a command errors out in a subprocess. (`#6651 <https://github.com/pypa/pip/issues/6651>`_)
    +
    +Vendored Libraries
    +------------------
    +
    +- Upgrade certifi to 2019.6.16
    +- Upgrade distlib to 0.2.9.post0
    +- Upgrade msgpack to 0.6.1
    +- Upgrade requests to 2.22.0
    +- Upgrade urllib3 to 1.25.3
    +- Patch vendored html5lib, to prefer using `collections.abc` where possible.
    +
    +Improved Documentation
    +----------------------
    +
    +- Document how Python 2.7 support will be maintained. (`#6726 <https://github.com/pypa/pip/issues/6726>`_)
    +- Upgrade Sphinx version used to build documentation. (`#6471 <https://github.com/pypa/pip/issues/6471>`_)
    +- Fix generation of subcommand manpages. (`#6724 <https://github.com/pypa/pip/issues/6724>`_)
    +- Mention that pip can install from git refs. (`#6512 <https://github.com/pypa/pip/issues/6512>`_)
    +- Replace a failing example of pip installs with extras with a working one. (`#4733 <https://github.com/pypa/pip/issues/4733>`_)
    +
     19.1.1 (2019-05-06)
     ===================
     
    
  • news/urllib3.vendor+0 1 removed
    @@ -1 +0,0 @@
    -Upgrade urllib3 to 1.25.3
    
a4c735b14a62

FIX #6413 pip install <url> allow directory traversal

https://github.com/gzpan123/pipgzpan123Apr 17, 2019via ghsa
3 files changed · +114 5
  • news/6413.bugfix+3 0 added
    @@ -0,0 +1,3 @@
    +Prevent ``pip install <url>`` from permitting directory traversal if e.g.
    +a malicious server sends a ``Content-Disposition`` header with a filename
    +containing ``../`` or ``..\\``.
    
  • src/pip/_internal/download.py+26 5 modified
    @@ -66,7 +66,8 @@
                '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__)
    @@ -1050,6 +1051,29 @@ def unpack_url(
             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,  # type: Link
         session,  # type: PipSession
    @@ -1097,10 +1121,7 @@ def _download_http_url(
         # 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)
    
  • tests/unit/test_download.py+85 0 modified
    @@ -12,6 +12,7 @@
     import pip
     from pip._internal.download import (
         CI_ENVIRONMENT_VARIABLES, MultiDomainBasicAuth, PipSession, SafeFileCache,
    +    _download_http_url, parse_content_disposition, sanitize_content_filename,
         unpack_file_url, unpack_http_url, url_to_path,
     )
     from pip._internal.exceptions import HashMismatch
    @@ -199,6 +200,90 @@ def test_unpack_http_url_bad_downloaded_checksum(mock_unpack_file):
             rmtree(download_dir)
     
     
    +@pytest.mark.parametrize("filename, expected", [
    +    ('dir/file', 'file'),
    +    ('../file', 'file'),
    +    ('../../file', 'file'),
    +    ('../', ''),
    +    ('../..', '..'),
    +    ('/', ''),
    +])
    +def test_sanitize_content_filename(filename, expected):
    +    """
    +    Test inputs where the result is the same for Windows and non-Windows.
    +    """
    +    assert sanitize_content_filename(filename) == expected
    +
    +
    +@pytest.mark.parametrize("filename, win_expected, non_win_expected", [
    +    ('dir\\file', 'file', 'dir\\file'),
    +    ('..\\file', 'file', '..\\file'),
    +    ('..\\..\\file', 'file', '..\\..\\file'),
    +    ('..\\', '', '..\\'),
    +    ('..\\..', '..', '..\\..'),
    +    ('\\', '', '\\'),
    +])
    +def test_sanitize_content_filename__platform_dependent(
    +    filename,
    +    win_expected,
    +    non_win_expected
    +):
    +    """
    +    Test inputs where the result is different for Windows and non-Windows.
    +    """
    +    if sys.platform == 'win32':
    +        expected = win_expected
    +    else:
    +        expected = non_win_expected
    +    assert sanitize_content_filename(filename) == expected
    +
    +
    +@pytest.mark.parametrize("content_disposition, default_filename, expected", [
    +    ('attachment;filename="../file"', 'df', 'file'),
    +])
    +def test_parse_content_disposition(
    +    content_disposition,
    +    default_filename,
    +    expected
    +):
    +    actual = parse_content_disposition(content_disposition, default_filename)
    +    assert actual == expected
    +
    +
    +def test_download_http_url__no_directory_traversal(tmpdir):
    +    """
    +    Test that directory traversal doesn't happen on download when the
    +    Content-Disposition header contains a filename with a ".." path part.
    +    """
    +    mock_url = 'http://www.example.com/whatever.tgz'
    +    contents = b'downloaded'
    +    link = Link(mock_url)
    +
    +    session = Mock()
    +    resp = MockResponse(contents)
    +    resp.url = mock_url
    +    resp.headers = {
    +        # Set the content-type to a random value to prevent
    +        # mimetypes.guess_extension from guessing the extension.
    +        'content-type': 'random',
    +        'content-disposition': 'attachment;filename="../out_dir_file"'
    +    }
    +    session.get.return_value = resp
    +
    +    download_dir = tmpdir.join('download')
    +    os.mkdir(download_dir)
    +    file_path, content_type = _download_http_url(
    +        link,
    +        session,
    +        download_dir,
    +        hashes=None,
    +        progress_bar='on',
    +    )
    +    # The file should be downloaded to download_dir.
    +    actual = os.listdir(download_dir)
    +    assert actual == ['out_dir_file']
    +
    +
     @pytest.mark.parametrize("url,win_expected,non_win_expected", [
         ('file:tmp', 'tmp', 'tmp'),
         ('file:c:/path/to/file', r'C:\path\to\file', 'c:/path/to/file'),
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

11

News mentions

0

No linked articles in our index yet.