THE LINUX FOUNDATION PROJECTS
Blog

The Python Package Index has Completed its Second Audit

This blog post was originally published to the Python Package Index blog by Mike Fiedler, the PyPI Safety & Security Engineer.

In 2023 PyPI completed its first security audit, and PyPI is proud to announce that we have completed our second external security audit. This work was funded by the Sovereign Tech Agency, a supporter of Open Source security-related improvements, partnering with Trail of Bits to perform the audit. Thanks to ongoing support from Alpha-Omega, my role at the PSF enabled me to focus on rapid remediation of the findings.

This time around, there’s no three-part series, as the scope was narrower, focused only on PyPI’s codebase and behaviors. Read on for a summary of issues identified, their resolutions, and more details about the audit process.

The full audit report can be found on the Trail of Bits publication page. We highly recommend reading that for the fullest context first.

Findings

There were 14 total findings in the audit report. Of the 14 findings, we prioritized remediation based on Severity and Difficulty, and which reports to accept for now. There were no Critical severity findings, 2 High, 1 Medium, 7 Low, and 3 Informational severity findings.

All but 2 findings have been remediated, and the remaining 2 are accepted for now. More details on the accepted findings below, but in general these were accepted because they require significant effort to remediate, and the risk they pose is relatively low.

To reiterate, the published report PDF goes into deeper detail about each finding, so we recommend reading that for the fullest context first.

Details

For some of the Remediated entries and all the Accepted ones, we’ll go into more detail below.

TOB-PYPI26-1: OIDC JTI anti-replay lock expires before JWT leeway window closes

PyPI’s Trusted Publishing flow uses OIDC JWTs issued by CI providers to mint short-lived upload tokens. Each JWT contains a jti (JWT Token Identifier) claim that should be single-use. To enforce this, we store each jti in cache (Redis) with an expiration of exp + 5 seconds, and check whether it already exists before accepting a new token.

The problem: PyJWT is configured with leeway=30, meaning it accepts tokens up to 30 seconds past their exp claim. This created a 25-second window (from exp + 5 to exp + 30) where the cache key had already been evicted, but the JWT still passed signature verification. During that window, a replayed token would pass both the signature check and the jti uniqueness check.

The fix was straightforward — align the cache TTL to outlive the full leeway window by setting the expiration to exp + leeway + margin. I also took the opportunity to centralize these time-window constants so they’re derived from a shared configuration, preventing future drift when one value is updated without the other.

TOB-PYPI26-6: IP ban bypass via macaroon API token authentication

Accepted for now.

PyPI administrators can ban IP addresses through the admin dashboard. The session authentication policy enforces this by checking the IP against the ban list before returning an identity. However, the macaroon (API token) authentication policy doesn’t perform this same check. This means a user with a valid API token could continue uploading packages from a banned IP address.

We’ve accepted this finding for now. IP bans are a relatively blunt tool that we use sparingly, introduced late last year to mitigate a specific wave of abuse. The practical risk here is low – if we’ve identified a malicious actor, we have other mechanisms to disable their account entirely. That said, it’s a gap worth closing, and we’ll likely address it as part of broader work on making security controls consistent across all authentication methods.

TOB-PYPI26-8: Organization members can invite new owners due to a missing manage permission check

This was the highest-severity finding in the audit, and one Mike prioritized immediately.

The manage_organization_roles view handled both GET (viewing the people page) and POST (sending invitations) under a single @view_config decorator that only required OrganizationsRead permission. This meant any organization member could send invitations with any role – including Owner – to any PyPI user.

The irony is that we already had the correct pattern elsewhere in the codebase. Views like resend_organization_invitation and change_organization_role correctly use separate @view_config decorators for GET and POST with distinct permission requirements. This one was simply missed.

The fix was to split the view configurationGET requires OrganizationsReadPOST requires OrganizationsManage. As part of the audit, Trail of Bits also developed a custom CodeQL query to detect this class of issue – views that handle state-changing POST requests under a read-only permission check. I’ll integrate that into our CI to catch this pattern going forward.

TOB-PYPI26-10: Wheel METADATA is served to installers without validation against upload metadata

Accepted for now.

This is a nuanced one. When a wheel is uploaded to PyPI, we store two independent sources of metadata: the form-declared metadata from the upload request (which populates the database and the JSON API), and the embedded .dist-info/METADATA file extracted from the wheel itself (which is served via PEP 658 to pip for dependency resolution).

These two sources are never compared. In theory, an attacker could embed hidden dependencies in the wheel’s METADATA that pip would install, but that security tools querying the JSON API would never see.

We’ve accepted this for now because the fix is non-trivial. Properly validating embedded metadata against upload metadata touches a core part of how we handle uploads, and requires careful consideration of edge cases across the ecosystem. This is something we want to get right rather than rush, and involves a fair amount of database changes, including data backfills.

TOB-PYPI26-13: Organization-scoped project associations persist after project transfer or removal

This was the other High-severity finding, and a subtle one.

When a project is transferred between organizations, the OrganizationProject junction record is correctly deleted and recreated. However, the TeamProjectRole records – which grant a team’s members access to specific projects – were not cleaned up during the transfer.

This meant that if LexCorp Organization had a “release-engineers” team with Owner-level access to a project, and that project was transferred to Organization OsCorp, the LexCorp team’s members would silently retain full access to the project. Worse, the receiving organization had no visibility into these stale associations – team-granted permissions are resolved at ACL evaluation time and don’t appear as individual collaborator entries in the UI.

The fix in pypi/warehouse#19749 ensures that TeamProjectRole records belonging to the departing organization are cleaned up when a project is transferred. Auditing database records proved that this has not happened in the past, so I am confident there have been no such transfers with dangling permissions. Mike also added defensive filters in the project’s ACL computation to verify that a team’s organization matches the project’s current organization before granting permissions, so stale records can’t grant access regardless of how they’re orphaned.

Summary

Working with Trail of Bits was again a pleasure. The team were thorough, communicative, and clearly understood the nuances of a system like PyPI – where the threat model spans everything from CI/CD token replay to metadata integrity for millions of downstream users.

Beyond the 14 findings, the audit also produced proposal reviews for features we are considering (per-org Trusted Publishers, TOTP hardening, and more), as well as custom CodeQL queries to integrate into our CI/CD pipeline.

This audit was funded in partnership with the Sovereign Tech Agency, which continues to support security improvements across the Open Source ecosystem.

About the Author

Mike Fiedler, PyPI Safety and Security Engineer

With over thirty years of software and systems experience, Mike is a seasoned professional who has accumulated extensive knowledge and expertise in the field. He has actively engaged with the Python community, contributing to open source projects and sharing his insights. His leadership roles at companies like Datadog, Warby Parker, and others have enabled him to mentor and guide others in the tech industry. Recognized as an AWS Container Hero and an avid open source maintainer, Mike’s dedication to learning, problem-solving reflects his holistic approach to technology. Beyond his professional pursuits, Mike’s personal life is just as vibrant. He has been a dedicated volunteer roller derby referee for the past 15 years, and he enjoys experimenting with various vegetarian dishes in the kitchen. Mike currently resides in New York City with his partner, Elyssa.

Mike Fiedler’s work at the Python Software Foundation is supported by Alpha-Omega.