Socket proactively blocks malicious open source packages in your code.
Secure your dependencies with us
PyPI has fixed two high-severity flaws found during its second external security audit, addressing access control issues that could have allowed organization members to invite new owners and stale team permissions to persist after project transfers.
The audit was performed by Trail of Bits and funded by the Sovereign Tech Agency. It reviewed Warehouse, the open source Python application that powers PyPI and handles package uploads, metadata validation, storage, and downloads for pip and other installers. The assessment produced 14 findings: two High, one Medium, seven Low, and four Informational. All but two have been remediated.
Trail of Bits conducted the review from February 23 to March 20, 2026, with two consultants working over six engineer-weeks. The assessment focused on authentication and authorization, the OIDC trusted publishing pipeline, package upload and metadata validation, organization permission boundaries, API token lifecycle operations, and audit logging.
Organization Members Could Invite Owners#
The first high-severity issue involved PyPI’s organization role management flow.
Trail of Bits found that the manage_organization_roles view handled both GET and POST requests under a single @view_config decorator requiring only Permissions.OrganizationsRead. On POST, the view called _send_organization_invitation, creating an invitation with the submitted role name.
Because the view required only read permission, any organization member could send invitations with any role, including Owner, Manager, or Billing Manager. In an exploit scenario described by Trail of Bits, a user with the Member role could submit a POST request with role_name set to Owner and target a colluding account. The application would create a signed invitation token granting the Owner role and email it to the target user.
If accepted, the invitation would give the target administrative control over the organization, including the ability to add and remove projects, manage trusted publishers, modify billing, and remove the original owner.
PyPI fixed the issue by splitting the view configuration so GET requests require OrganizationsRead, while POST requests require OrganizationsManage. Trail of Bits also developed a custom CodeQL query to detect similar cases where state-changing POST handlers are protected only by read-level permissions.
Project Transfers Could Leave Stale Upload Access#
The second high-severity finding involved project transfers between organizations.
When a project was transferred or removed from an organization, PyPI’s delete_organization_project method deleted only the OrganizationProject junction record. It did not clean up organization-scoped TeamProjectRole records associated with the project.
Trail of Bits found that the TeamProjectRole model referenced projects and teams independently, with no constraint tying a team’s project access to the organization that currently owned the project. During ACL evaluation, PyPI queried all TeamProjectRole records for a project and granted permissions to members of each matched team without verifying that the team belonged to the project’s current organization.
This means stale team roles could continue granting access after a project changed organizations. A stale Owner-level role granted administrative permissions, while a stale Maintainer role granted ProjectsUpload, which Trail of Bits noted was sufficient to push malicious releases.
The stale access was also difficult to detect. Trail of Bits reported that the records were not visible to the receiving organization, did not appear in the project’s collaborator list, and were resolved only during ACL evaluation. In the report’s example, an attacker in a departing organization’s “release-engineers” team could retain Owner-level access after a project was transferred to a new organization, then upload a backdoored release using the retained upload permission.
PyPI fixed the issue by deleting TeamProjectRole records belonging to the departing organization before deleting the OrganizationProject junction record. It also added defensive ACL filters to verify that a team’s organization matches the project’s current organization before granting team permissions. PyPI said it audited database records and found no evidence that previous transfers had resulted in dangling permissions.
Trusted Publishing Replay Issues#
Trail of Bits also found two issues in PyPI’s Trusted Publishing flow, which uses OIDC JWTs from CI providers such as GitHub Actions and GitLab CI to mint short-lived upload tokens.
The Medium-severity issue involved PyPI’s handling of the JWT Token Identifier, or jti, claim. The claim is intended to make each OIDC token single-use. PyPI stored each jti in Redis after minting a macaroon and checked whether it already existed before accepting a new token.
The Redis key expired at exp + 5, where exp is the token’s expiration timestamp. But PyJWT accepted tokens up to 30 seconds past expiration because jwt.decode was called with leeway=30. That created a 25-second window in which Redis had evicted the anti-replay key, while the JWT still passed signature verification.
Trail of Bits said an attacker who obtained a valid GitHub Actions OIDC JWT, such as through a leaked CI log, misconfigured artifact, or compromised self-hosted runner, could wait until five seconds after the token’s expiration and repeatedly submit it during the remaining leeway period. Each request could pass the signature check and the replay check, minting fresh 15-minute upload macaroons.
PyPI fixed the issue by aligning the Redis key expiration with the full JWT leeway window and centralizing the relevant time-window constants.
A related Low-severity finding involved a time-of-check to time-of-use race in the same anti-replay flow. The minting path split jti protection across two Redis operations: an EXISTS check before macaroon creation and a later SET NX after the macaroon was created. Because the return value of SET NX was not checked, two concurrent requests carrying the same jti could both mint valid upload tokens. PyPI says this issue has also been remediated.
Wheel Metadata Validation Gap Remains Open#
During upload, PyPI constructs release metadata from HTTP form POST fields. For wheel uploads, it separately extracts the embedded .dist-info/METADATA file from the wheel archive, writes it to disk, hashes it, and stores it alongside the wheel for PEP 658 serving. Trail of Bits found that the embedded metadata is never parsed or validated against the form-derived metadata.
That creates two independent metadata sources. The database metadata generated from the POST request populates PyPI’s JSON API and project page. The wheel METADATA file is served through the Simple API as <filename>.metadata, which pip 22.3 and later can download for dependency resolution instead of fetching the full wheel.
Trail of Bits described a scenario where a package declares no dependencies through the upload form but embeds Requires-Dist: evil-helper>=1.0 inside the wheel metadata. In that case, the JSON API and PyPI project page would show no dependencies. Security tools that query the JSON API, including pip-audit or SBOM generators, could report zero dependencies. But when a user runs pip install, pip could fetch the PEP 658 metadata file, read the embedded Requires-Dist, resolve the additional dependency, and install it.
Trail of Bits recommended parsing the embedded wheel metadata after extraction and, at minimum, comparing the Name, Version, and Requires-Dist fields against the form-derived metadata. Uploads should be rejected when those fields disagree.
PyPI accepted the finding for now. In its public summary, PyPI said the fix is non-trivial because validating embedded metadata against upload metadata touches core upload behavior and requires careful handling of ecosystem edge cases, database changes, and backfills.
Access control issues accounted for eight of the 14 findings, spanning read permissions applied to write operations, object IDs used without ownership validation, and ACL logic that trusted stale relationship records.
That pattern appeared in the high-severity organization invitation issue, where a write action was protected only by OrganizationsRead; the project transfer issue, where ACL evaluation trusted stale team-role records; and a Low-severity API token deletion issue, where PyPI deleted tokens by macaroon_id without verifying ownership.
Other findings covered a missing write permission check for organization applications, audit logging gaps, a verification badge bypass, a TOTP replay issue, and issuer isolation for custom GitHub Enterprise Server OIDC publishers.
Package registry security depends on implementation details that are not always visible to package consumers: permission checks on organization routes, object ownership validation in service-layer methods, token replay windows in OIDC publishing flows, and metadata consistency across APIs used by installers and security tools.
If installers and security tools consume different metadata sources, defenders may not see the same dependency information that package managers use during installation.
The audit shows why sustained funding matters for open source security: the Sovereign Tech Agency funded the review, and PyPI credited Alpha-Omega’s support with giving maintainers time to focus on remediation.