Skip to content

Instantly share code, notes, and snippets.

@adeadfed
Last active July 13, 2023 00:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adeadfed/ccc834440af354a5638f889bee34bafe to your computer and use it in GitHub Desktop.
Save adeadfed/ccc834440af354a5638f889bee34bafe to your computer and use it in GitHub Desktop.
Dependency confusion in pipreqs

Info

Dependency confusion in pipreqs

Software Linkpipreqs
Affected Versions0.3.0 - 0.4.12
Tested onpipreqs 0.4.11
Vulnerable Componentspipreqs/pipreqs.py#L447-L449
CVSS 3.1CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CVECVE-2023-31543

Summary

Pipreqs's remote dependency resolving mechanism (lines 447-449) can be abused to inject arbitrary packages into the final requirements.txt file, leading to a dependency confusion attack and subsequent arbitrary code execution on machines running the Python code with requirements, generated by the vulnerable version of pipreqs.

There are three important conditions that have to be met by an attacker to perform the package confusion. (see details section)

Mitigation

The issue was mitigated in the minor pipreqs 0.4.13 release on April 14th 2023. pipreqs now has an improved local dependency resolving mechanism that accounts for possible PyPI package <-> Python module name mismatches. Additionally, when the remote resolving mechanism is used, the application will put up a warning message to ensure the developers will manually review the output.

Merge request with corresponding fixes

Details

The attack exploits the mismatches in the naming of PyPI packages and the nested Python modules within. Necessary prerequisites that would allow an attacker to attack a given PyPI package include:

  • PyPI package name (later - pypi_package_name) and the names of its exported Python modules (later on exported_module_name) must differ.
  • exported_module_name:pypi_package_name mapping must be absent from the pipreq's hard-coded mapping file
  • exported_module_name must be available at PyPI as a package name

Let’s use the djangorestframework-simplejwt package as an example. It has over 1.3M monthly downloads, according to PyPIStats (https://pypistats.org/packages/djangorestframework-simplejwt). The package exports the rest_framework_simplejwt Python module.

Consider the following snippet of code from the above package's documentation.

from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    ...
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    ...
]

When pipreqs is run on the code above, the following things happen:

  1. Pipreqs extracts the imported module name (rest_framework_simplejwt) and places it into the candidates variable at line 425.

    Untitled

  2. Pipreqs tries to find the rest_framework_simplejwt value in the mapping file at line 429 through the get_pkg_names function, but it isn’t there, so the script assumes that the package name is rest_framework_simplejwt.

    Untitled

  3. The script tries to find the rest_framework_simplejwt module in the exports of all locally installed Python packages at line 445 by default.

    If the package djangorestframework-simplejwt is installed locally, the function will find it and assign it to the local variable.

    Untitled

    If not, the function will return an empty array.

    Untitled

  4. Pipreqs then compares the names inside candidates and local variables to populate a difference variable at line 447.

    Since the candidates variable contains rest_framework_simplejwt, and the local variable is either empty or has the name of the PyPI package (djangorestframework-simplejwt), this comparison will always evaluate to True. Consequently, the code will simply copy the candidates entries into the difference variable.

    Untitled

  5. Then get_imports_info function is triggered with the differencearray (['rest_framework_simplejwt']). It tries to find PyPI packages with the same names as in the passed array.

    If a PyPI package name inside the difference array is missing from PyPI, the code will ignore it. However, if an attacker registers the name on PyPI, pipreqs will inject this malicious package to "requirements.txt". And, during the dependency installation, attacker-controlled code will be executed on the user's machine.

Proof of Concept

I've created a PoC using the rest_framework_simplejwt module mentioned above (https://pypi.org/project/rest-framework-simplejwt/).

If you run pipreqs on the code above, you will see that my package is injected into the requirements.txt:

pipreqs --print
djangorestframework_simplejwt==5.2.2 <--- legitimate package 
rest_framework_simplejwt==0.0.2      <--- malicious package

INFO: Successfully output requirements
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment