The Python Package Index (PyPI) has successfully patched two high-severity vulnerabilities uncovered during its second external security audit. The flaws rooted in broken access control and stale permission persistence could have allowed malicious actors to seize administrative control of organizations.
The audit assessed Warehouse, the open-source application powering PyPI, between February 23 and March 20, 2026. Consultants worked across six engineering weeks.
Examining authentication and authorization flows, the OIDC trusted publishing pipeline, package upload and metadata validation, organization permission boundaries, API token lifecycle operations, and audit logging. The review produced 14 findings in total: two High, one Medium, seven Low, and four Informational, with 12 of 14 now remediated.
Flaw 1: Organization Members Could Escalate to Owner Role
The first critical vulnerability resided in PyPI’s organization role management view (manage_organization_roles). Trail of Bits discovered that both GET and POST requests were handled under a single @view_config decorator that only required Permissions.OrganizationsRead a read-level permission accessible to any standard organization member.
Because POST requests, which trigger role invitations, were never separately gated behind write-level permissions, any organization member could craft a POST request role_name=Owner and target a colluding external account. PyPI’s backend would then generate a signed invitation token granting full Owner privileges and email it to the target.
If accepted, the attacker would gain complete administrative control: adding or removing projects, managing trusted publishers, modifying billing, and even ejecting the original owner from the organization. The exploit required no special privileges, just membership in the target organization.
PyPI resolved this by splitting the view configuration so that GET requests require OrganizationsRead , while POST requests now mandate the higher OrganizationsManage permission, Bits additionally developed a custom CodeQL query to detect similar patterns across the codebase in which read-only permission checks protect write-triggering POST handlers.
Flaw 2: Stale Team Permissions Persisted After Project Transfers
The second high-severity finding exposed a dangerous residual access flaw in PyPI’s project transfer logic. When a project was transferred between organizations, the delete_organization_project method only deleted the OrganizationProject The junction record was not clean up TeamProjectRole records that linked teams to the project.
Because the TeamProjectRole model referenced projects and teams independently with no constraint tying team access to the currently owning organization, stale role records remained in the database after transfers.
Critically, PyPI’s ACL evaluation engine queried all TeamProjectRole entries for a given project and granted permissions to matched team members without first verifying that those teams belonged to the project’s current organization.
This meant a threat actor embedded in a departing organization’s “release-engineers” team could retain ProjectsUpload (Maintainer-level) or even Owner-level access after the project was silently moved to a new organization.
Since these stale records were invisible to the receiving organization and did not appear in the project’s collaborator list. The attacker could then push a backdoored release using the retained upload permission.
PyPI fixed the issue by deleting TeamProjectRole records belonging to the departing organization before removing the OrganizationProject record.
Defensive ACL filters were also added to cross-check that a team’s organization matches the project’s current owner before granting permissions. PyPI audited existing database records and confirmed no prior transfers had resulted in dangling permissions.
OIDC JWT Replay Window in Trusted Publishing
A flagged medium-severity flaw in PyPI’s Trusted Publishing flow, which uses OIDC JWTs from CI/CD providers like GitHub Actions and GitLab CI to mint short-lived upload tokens (macaroons).
The anti-replay mechanism stored each JWT’s jti (Token Identifier) claim in Redis, set to expire at exp + 5 seconds. However, PyJWT was configured with leeway=30, meaning it accepted tokens up to 30 seconds past expiration.
This created a 25-second window in which Redis had already evicted the anti-replay key, yet the JWT still passed signature verification, allowing repeated token submissions to mint fresh 15-minute upload macaroons.
An attacker who obtained a valid OIDC JWT via a leaked CI log, misconfigured artifact, or compromised self-hosted runner could exploit this window to generate multiple valid upload tokens from a single stolen credential. PyPI fixed the issue by aligning Redis key expiration with the full JWT leeway window and centralizing time-window constants.
One significant finding remains unpatched: a metadata validation gap in wheel uploads. PyPI constructs release metadata from HTTP form fields at upload time, but separately extracts and stores the .dist-info/METADATA file embedded in the wheel archive for PEP 658 serving without ever comparing the two sources.
This creates a split-metadata attack surface. A malicious package author could declare zero dependencies in the upload form (visible via the JSON API and PyPI project page), while embedding Requires-Dist: evil-helper>=1.0 inside the wheel’s metadata file.
Security tools like pip-audit or SBOM generators querying the JSON API would report no dependencies, while pip install would silently resolve and install the hidden malicious dependency via PEP 658 metadata.
PyPI acknowledged the finding but noted the fix is non-trivial, requiring careful handling of ecosystem edge cases, database changes, and backfills to core upload behavior.
Access control failures accounted for 8 of the 14 total findings, including read permissions applied to write operations, object IDs used without ownership validation, and ACL logic that trusts stale relational records, Socket reported.
FAQ
Q1: What were the two high-severity flaws fixed by PyPI in its 2026 security audit?
PyPI patched a broken access-control flaw that allowed any organization member to invite Owner-level users, and a stale permission bug that persisted team upload access after project transfers to new organizations.
Q2: How could the stale TeamProjectRole vulnerability be exploited to push malicious packages?
An attacker retaining a stale Maintainer or Owner team role after a project transfer could use the preserved ProjectsUpload permission to push backdoored releases to PyPI without the receiving organization’s knowledge.
Q3: What is the OIDC JWT replay vulnerability in PyPI’s Trusted Publishing pipeline?
A timing mismatch between Redis key expiration (exp + 5) and PyJWT’s 30-second leeway window created a 25-second gap where a stolen GitHub Actions OIDC token could be replayed to mint multiple valid upload macaroons.
Q4: Why does the unpatched wheel metadata discrepancy pose a supply chain security risk?
It allows a package to show zero dependencies in the JSON API while embedding hidden Requires-Dist entries in the wheel’s PEP 658 metadata file, causing pip to install undisclosed dependencies during installation silently.
Site: thecybrdef.com
For more insights and updates, follow us on Google News, Twitter, and LinkedIn.