Dependency confusion in pipreqs
Software Link | pipreqs |
Affected Versions | 0.3.0 - 0.4.12 |
Tested on | pipreqs 0.4.11 |
Vulnerable Components | pipreqs/pipreqs.py#L447-L449 |
CVSS 3.1 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
CVE | CVE-2023-31543 |
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)
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
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 onexported_module_name
) must differ. exported_module_name
:pypi_package_name
mapping must be absent from the pipreq's hard-coded mapping fileexported_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:
-
Pipreqs extracts the imported module name (
rest_framework_simplejwt
) and places it into thecandidates
variable at line 425. -
Pipreqs tries to find the
rest_framework_simplejwt
value in the mapping file at line 429 through theget_pkg_names
function, but it isn’t there, so the script assumes that the package name isrest_framework_simplejwt
. -
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 thelocal
variable.If not, the function will return an empty array.
-
Pipreqs then compares the names inside
candidates
andlocal
variables to populate adifference
variable at line 447.Since the
candidates
variable containsrest_framework_simplejwt
, and thelocal
variable is either empty or has the name of the PyPI package (djangorestframework-simplejwt
), this comparison will always evaluate toTrue
. Consequently, the code will simply copy thecandidates
entries into thedifference
variable. -
Then
get_imports_info function
is triggered with thedifference
array (['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.
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