Skip to content
This repository has been archived by the owner on Jul 16, 2020. It is now read-only.

PyPI compromise and pip without TUF

Trishank Karthik Kuppusamy edited this page Sep 21, 2013 · 5 revisions

In the event of a PyPI compromise, we want to see how pip is affected. When and how would an attacker be able to coax pip into install malicious packages?

First, we set up the virtual environment (for cleanroom testing) and install pip-without-TUF:

$ cd  /tmp
$ curl -O https://pypi.python.org/packages/source/v/virtualenv/virtualenv-1.10.1.tar.gz
$ tar xvfz virtualenv-1.10.1.tar.gz
$ python virtualenv-1.10.1/virtualenv.py --no-site-packages pypi-compromise-without-tuf
$ source pypi-compromise-without-tuf/bin/activate

First, suppose that we introduce the FooBar package on PyPI-with-TUF by having the "unclaimed" targets sign for it. This means that the developers of FooBar have not yet "claimed" ownership (and thus responsibility) for the package, so PyPI will use an online key to sign for all "unclaimed" targets. (This key is online to allow for continuous release of "unclaimed" PyPI targets.) This is the lowest level of security for a package on PyPI-with-TUF: we will protect the package from being tampered by a mirror or a CDN, but we cannot protect it from being tampered by an attacker who has compromised PyPI.

$ pip install FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.unclaimed.good/targets/simple/
Downloading/unpacking FooBar
  Downloading FooBar-0.1.tar.gz
  Running setup.py egg_info for package FooBar
    
Installing collected packages: FooBar
  Running setup.py install for FooBar
    FooBar 0.1
    
Successfully installed FooBar
Cleaning up...

Let us see what happens when an attacker compromises PyPI and tampers with the FooBar package targets and signs for them with the online "unclaimed" targets role key:

$ pip install --upgrade FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.unclaimed.bad/targets/simple/
Downloading/unpacking FooBar from http://mirror1.poly.edu/test-pip/pypi-compromise/repository.unclaimed.bad/targets/packages/source/F/FooBar/FooBar-0.2.tar.gz#md5=10831baad99c6acbcd59103b1099d13c
  Downloading FooBar-0.2.tar.gz
  Running setup.py egg_info for package FooBar
    
Installing collected packages: FooBar
  Found existing installation: FooBar 0.1
    Uninstalling FooBar:
      Successfully uninstalled FooBar
  Running setup.py install for FooBar
    TAMPERED FooBar 0.2
    
Successfully installed FooBar
Cleaning up...

In summary, the "unclaimed" targets role will allow for continuous release of PyPI packages, as is the case of PyPI-without-TUF today. However, unlike PyPI-without-TUF, it will protect all "unclaimed" PyPI packages from attacks by mirrors or CDNs. (An "unclaimed" PyPI package is one for which its developers have not registered their own signing keys to sign for all TUF metadata about that package.) Nevertheless, since the "unclaimed" targets role key is online to allow continuous release, any compromise of PyPI will not protect "unclaimed" targets. (We will soon see how to protect PyPI packages even against a compromise of PyPI with the "claimed" targets role.)

Now, suppose that developers of the FooBar package register their signing keys (to sign for all FooBar targets) with PyPI-with-TUF. PyPI-with-TUF will then delegate all FooBar targets from the "recently-claimed" targets role to the FooBar targets role. If a target is available in both the "recently-claimed" and "unclaimed" targets roles, "recently-claimed" would be preferred over "unclaimed" to be responsible for the target:

$ pip install --upgrade FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.recently-claimed.good/targets/simple/
Downloading/unpacking FooBar from http://mirror1.poly.edu/test-pip/pypi-compromise/repository.recently-claimed.good/targets/packages/source/F/FooBar/FooBar-0.3.tar.gz#md5=eb534887d5d531c594715bd929c6c789
  Downloading FooBar-0.3.tar.gz
  Running setup.py egg_info for package FooBar
    
Installing collected packages: FooBar
  Found existing installation: FooBar 0.2
    Uninstalling FooBar:
      Successfully uninstalled FooBar
  Running setup.py install for FooBar
    FooBar 0.3
    
Successfully installed FooBar
Cleaning up...

However, it is important to note that "recently-claimed" does not offer less or more security than "unclaimed". "recently-claimed" allows for the continuous release of PyPI packages for which their respective developers have registered their own package-signing keys; therefore, the signing key for "recently-claimed", too, must remain online. Here is an example of an attacker who has compromised PyPI and modifies the delegation of the FooBar package in "recently-claimed":

$ pip install --upgrade FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.recently-claimed.bad/targets/simple/
Downloading/unpacking FooBar from http://mirror1.poly.edu/test-pip/pypi-compromise/repository.recently-claimed.bad/targets/packages/source/F/FooBar/FooBar-0.4.tar.gz#md5=5d21b9feebf2118d3e0916cbfce22176
  Downloading FooBar-0.4.tar.gz
  Running setup.py egg_info for package FooBar
    
Installing collected packages: FooBar
  Found existing installation: FooBar 0.3
    Uninstalling FooBar:
      Successfully uninstalled FooBar
  Running setup.py install for FooBar
    TAMPERED FooBar 0.4
    
Successfully installed FooBar
Cleaning up...

What is the point of "recently-claimed", then? It exists so that PyPI-with-TUF may continuously release packages for which developers have registered their own package-signing keys. Once in a while, PyPI-with-TUF will strongly secure the delegations in "recently-claimed" by promoting them to the "claimed" targets role.

Suppose that PyPI-with-TUF has promoted the FooBar package from "recently-claimed" to "claimed". If a target is available in the "claimed", "recently-claimed", and "unclaimed" targets roles, then "claimed" would be the most preferred role to be responsible for the target:

$ pip install --upgrade FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.claimed.good/targets/simple/
Downloading/unpacking FooBar from http://mirror1.poly.edu/test-pip/pypi-compromise/repository.claimed.good/targets/packages/source/F/FooBar/FooBar-0.5.tar.gz#md5=02ccbb78795c3671b7f3b856ec9a6951
  Downloading FooBar-0.5.tar.gz
  Running setup.py egg_info for package FooBar
    
Installing collected packages: FooBar
  Found existing installation: FooBar 0.4
    Uninstalling FooBar:
      Successfully uninstalled FooBar
  Running setup.py install for FooBar
    FooBar 0.5
    
Successfully installed FooBar
Cleaning up...

In this case, the pip-with-TUF user trusted PyPI to provide secure metadata about which package-signing keys the developers of the FooBar package intended for the package. This delegation metadata is secure because the "claimed" targets role key is offline.

Suppose that an attacker compromised PyPI and tampered with the TUF metadata for the FooBar package:

$ pip install --upgrade FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.claimed.bad/targets/simple/
Downloading/unpacking FooBar from http://mirror1.poly.edu/test-pip/pypi-compromise/repository.claimed.bad/targets/packages/source/F/FooBar/FooBar-0.6.tar.gz#md5=de819178b0c898756b6d1cc91bb24546
  Downloading FooBar-0.6.tar.gz
  Running setup.py egg_info for package FooBar
    
Installing collected packages: FooBar
  Found existing installation: FooBar 0.5
    Uninstalling FooBar:
      Successfully uninstalled FooBar
  Running setup.py install for FooBar
    TAMPERED FooBar 0.6
    
Successfully installed FooBar
Cleaning up...

Unfortunately, this user has now been compromised with a malicious FooBar package. This situation could have been avoided with the resilience to compromise offered by PyPI-with-TUF.