<?xml version="1.0" encoding="UTF-8" ?> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"> <channel> <title>The Python Package Index Blog</title><description>The official blog of the Python Package Index</description><link>https://blog.pypi.org/</link><atom:link href="https://blog.pypi.org/feed_rss_created.xml" rel="self" type="application/rss+xml" /> <docs>https://github.com/pypi/warehouse</docs><language>en</language> <pubDate>Wed, 15 Apr 2026 21:34:25 -0000</pubDate> <lastBuildDate>Wed, 15 Apr 2026 21:34:25 -0000</lastBuildDate> <ttl>1440</ttl> <generator>MkDocs RSS plugin - v1.17.9</generator> <image> <url>https://blog.pypi.org/assets/logo.png</url> <title>The Python Package Index Blog</title> <link>https://blog.pypi.org/</link> </image> <item> <title>PyPI has completed its second audit</title> <author>Mike Fiedler</author> <category>security</category> <category>transparency</category> <description>&lt;p&gt;In 2023 &lt;a href=&#34;../2023-11-14-1-pypi-completes-first-security-audit/&#34;&gt;PyPI completed its first security audit&lt;/a&gt;, and I am proud to announce that we have now completed our second external security audit.&lt;/p&gt; &lt;p&gt;This work was funded by the &lt;a href=&#34;https://www.sovereign.tech/&#34;&gt;Sovereign Tech Agency&lt;/a&gt;, a supporter of Open Source security-related improvements, partnering with &lt;a href=&#34;https://www.trailofbits.com/&#34;&gt;Trail of Bits&lt;/a&gt; to perform the audit. Thanks to ongoing support from &lt;a href=&#34;https://alpha-omega.dev/&#34;&gt;Alpha-Omega&lt;/a&gt;, my role at the PSF enabled me to focus on rapid remediation of the findings.&lt;/p&gt; &lt;p&gt;This time around, there&#39;s no three-part series, as the scope was narrower, focused only on PyPI&#39;s codebase and behaviors. Read on for a summary of issues identified, their resolutions, and more details about the audit process.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;The full audit report can be found on the &lt;a href=&#34;https://github.com/trailofbits/publications/blob/master/reviews/2026-04-pypi-warehouse-securityreview.pdf&#34;&gt;Trail of Bits publication page&lt;/a&gt;. I highly recommend reading that for the fullest context first.&lt;/p&gt; &lt;h2 id=&#34;findings&#34;&gt;Findings&lt;/h2&gt; &lt;p&gt;Here&#39;s a table of the findings, status, and links to the relevant pull requests where applicable:&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th style=&#34;text-align: left;&#34;&gt;ID&lt;/th&gt; &lt;th style=&#34;text-align: left;&#34;&gt;Title&lt;/th&gt; &lt;th style=&#34;text-align: left;&#34;&gt;Severity&lt;/th&gt; &lt;th style=&#34;text-align: left;&#34;&gt;Difficulty&lt;/th&gt; &lt;th style=&#34;text-align: left;&#34;&gt;Status&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;#tob-pypi26-1-oidc-jti-anti-replay-lock-expires-before-jwt-leeway-window-closes&#34;&gt;TOB-PYPI26-1&lt;/a&gt;&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;OIDC JTI anti-replay lock expires before JWT leeway window closes&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Medium&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19627&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;TOB-PYPI26-2&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;OIDC token minting is vulnerable to a TOCTOU race in JTI anti-replay&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Low&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19625&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;TOB-PYPI26-3&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Verification badge bypass on the home page and download URLs&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Low&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Low&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19628&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;TOB-PYPI26-4&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Project-level token deletion audit events silently dropped due to data structure mismatch&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Low&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Low&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19652&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;TOB-PYPI26-5&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Password reset leaks privileged account status&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Low&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19653&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;#tob-pypi26-6-ip-ban-bypass-via-macaroon-api-token-authentication&#34;&gt;TOB-PYPI26-6&lt;/a&gt;&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;IP ban bypass via macaroon API token authentication&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Informational&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Accepted&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;TOB-PYPI26-7&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Moderators can modify organization applications due to a missing write permission check&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Low&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19619&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;#tob-pypi26-8-organization-members-can-invite-new-owners-due-to-a-missing-manage-permission-check&#34;&gt;TOB-PYPI26-8&lt;/a&gt;&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Organization members can invite new owners due to a missing manage permission check&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Medium&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19610&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;TOB-PYPI26-9&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;TOTP replay prevention bypass via space normalization mismatch between validation and storage&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Informational&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19668&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;#tob-pypi26-10-wheel-metadata-is-served-to-installers-without-validation-against-upload-metadata&#34;&gt;TOB-PYPI26-10&lt;/a&gt;&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Wheel METADATA is served to installers without validation against upload metadata&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Low&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Low&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Accepted&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;TOB-PYPI26-11&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;IDOR in API Token Deletion Allows Any Authenticated User to Delete Other Users&#39; Macaroons&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Low&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19669&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;TOB-PYPI26-12&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;GitHub OIDC publisher lookup lacks issuer URL isolation for custom GHES issuers&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Informational&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19661&#34;&gt;Remediated&lt;/a&gt; &lt;a href=&#34;https://github.com/pypi/warehouse/pull/19661&#34;&gt;1&lt;/a&gt;, &lt;a href=&#34;https://github.com/pypi/warehouse/pull/19718&#34;&gt;2&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;#tob-pypi26-13-organization-scoped-project-associations-persist-after-project-transfer-or-removal&#34;&gt;TOB-PYPI26-13&lt;/a&gt;&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Organization-scoped project associations persist after project transfer or removal&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19749&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td style=&#34;text-align: left;&#34;&gt;TOB-PYPI26-14&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Admin flag changes lack audit logging&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;Informational&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;High&lt;/td&gt; &lt;td style=&#34;text-align: left;&#34;&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/19751&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Of the 14 findings, I used a combination of Severity and Difficulty to determine which ones to work on first, and which ones to accept for now.&lt;/p&gt; &lt;p&gt;There were &lt;strong&gt;no Critical&lt;/strong&gt; severity findings, 2 High, 1 Medium, 7 Low, and 3 Informational severity findings.&lt;/p&gt; &lt;p&gt;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.&lt;/p&gt; &lt;p&gt;To reiterate, the &lt;a href=&#34;https://github.com/trailofbits/publications/blob/master/reviews/2026-04-pypi-warehouse-securityreview.pdf&#34;&gt;published report PDF&lt;/a&gt; goes into deeper detail about each finding, so I recommend reading that for the fullest context first.&lt;/p&gt; &lt;h2 id=&#34;details&#34;&gt;Details&lt;/h2&gt; &lt;p&gt;For some of the Remediated entries and all the Accepted ones, I&#39;ll go into more detail below.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi26-1-oidc-jti-anti-replay-lock-expires-before-jwt-leeway-window-closes&#34;&gt;TOB-PYPI26-1: OIDC JTI anti-replay lock expires before JWT leeway window closes&lt;/h3&gt; &lt;p&gt;PyPI&#39;s &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/&#34;&gt;Trusted Publishing&lt;/a&gt; flow uses OIDC JWTs issued by CI providers to mint short-lived upload tokens. Each JWT contains a &lt;code&gt;jti&lt;/code&gt; (JWT Token Identifier) claim that should be single-use. To enforce this, we store each &lt;code&gt;jti&lt;/code&gt; in cache (Redis) with an expiration of &lt;code&gt;exp + 5&lt;/code&gt; seconds, and check whether it already exists before accepting a new token.&lt;/p&gt; &lt;p&gt;The problem: &lt;code&gt;PyJWT&lt;/code&gt; is configured with &lt;code&gt;leeway=30&lt;/code&gt;, meaning it accepts tokens up to 30 seconds past their &lt;code&gt;exp&lt;/code&gt; claim. This created a 25-second window (from &lt;code&gt;exp + 5&lt;/code&gt; to &lt;code&gt;exp + 30&lt;/code&gt;) 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 &lt;code&gt;jti&lt;/code&gt; uniqueness check.&lt;/p&gt; &lt;p&gt;The fix was straightforward -- align the cache TTL to outlive the full leeway window by &lt;a href=&#34;https://github.com/pypi/warehouse/pull/19627/changes#diff-b6993869ec628f626827f05a87938a08f1ced03e52b68a49000100910d9d46f2R288&#34;&gt;setting the expiration to &lt;code&gt;exp + leeway + margin&lt;/code&gt;&lt;/a&gt;. I also took the opportunity to centralize these time-window constants so they&#39;re derived from a shared configuration, preventing future drift when one value is updated without the other.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi26-6-ip-ban-bypass-via-macaroon-api-token-authentication&#34;&gt;TOB-PYPI26-6: IP ban bypass via macaroon API token authentication&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;Accepted for now.&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;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&#39;t perform this same check. This means a user with a valid API token could continue uploading packages from a banned IP address.&lt;/p&gt; &lt;p&gt;I&#39;ve accepted this finding for now. IP bans are a relatively blunt tool that we use sparingly, &lt;a href=&#34;https://github.com/pypi/warehouse/pull/19213&#34;&gt;introduced late last year&lt;/a&gt; to mitigate a specific wave of abuse. The practical risk here is low - if we&#39;ve identified a malicious actor, we have other mechanisms to disable their account entirely. That said, it&#39;s a gap worth closing, and we&#39;ll likely address it as part of broader work on making security controls consistent across all authentication methods.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi26-8-organization-members-can-invite-new-owners-due-to-a-missing-manage-permission-check&#34;&gt;TOB-PYPI26-8: Organization members can invite new owners due to a missing manage permission check&lt;/h3&gt; &lt;p&gt;This was the highest-severity finding in the audit, and one I prioritized immediately.&lt;/p&gt; &lt;p&gt;The &lt;code&gt;manage_organization_roles&lt;/code&gt; view handled both &lt;code&gt;GET&lt;/code&gt; (viewing the people page) and &lt;code&gt;POST&lt;/code&gt; (sending invitations) under a single &lt;code&gt;@view_config&lt;/code&gt; decorator that only required &lt;code&gt;OrganizationsRead&lt;/code&gt; permission. This meant any organization member could send invitations with &lt;em&gt;any&lt;/em&gt; role - including Owner - to any PyPI user.&lt;/p&gt; &lt;p&gt;The irony is that we already had the correct pattern elsewhere in the codebase. Views like &lt;code&gt;resend_organization_invitation&lt;/code&gt; and &lt;code&gt;change_organization_role&lt;/code&gt; correctly use separate &lt;code&gt;@view_config&lt;/code&gt; decorators for &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;POST&lt;/code&gt; with distinct permission requirements. This one was simply missed.&lt;/p&gt; &lt;p&gt;The &lt;a href=&#34;https://github.com/pypi/warehouse/pull/19610&#34;&gt;fix was to split the view configuration&lt;/a&gt;: &lt;code&gt;GET&lt;/code&gt; requires &lt;code&gt;OrganizationsRead&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt; requires &lt;code&gt;OrganizationsManage&lt;/code&gt;. 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 &lt;code&gt;POST&lt;/code&gt; requests under a read-only permission check. I&#39;ll integrate that into our CI to catch this pattern going forward.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi26-10-wheel-metadata-is-served-to-installers-without-validation-against-upload-metadata&#34;&gt;TOB-PYPI26-10: Wheel METADATA is served to installers without validation against upload metadata&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;Accepted for now.&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;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 &lt;code&gt;.dist-info/METADATA&lt;/code&gt; file extracted from the wheel itself (which is served via &lt;a href=&#34;https://peps.python.org/pep-0658/&#34;&gt;PEP 658&lt;/a&gt; to &lt;code&gt;pip&lt;/code&gt; for dependency resolution).&lt;/p&gt; &lt;p&gt;These two sources are never compared. In theory, an attacker could embed hidden dependencies in the wheel&#39;s &lt;code&gt;METADATA&lt;/code&gt; that &lt;code&gt;pip&lt;/code&gt; would install, but that security tools querying the JSON API would never see.&lt;/p&gt; &lt;p&gt;We&#39;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 &lt;a href=&#34;https://github.com/pypi/warehouse/issues/8090&#34;&gt;involves a fair amount of database changes, including data backfills&lt;/a&gt;.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi26-13-organization-scoped-project-associations-persist-after-project-transfer-or-removal&#34;&gt;TOB-PYPI26-13: Organization-scoped project associations persist after project transfer or removal&lt;/h3&gt; &lt;p&gt;This was the other High-severity finding, and a subtle one.&lt;/p&gt; &lt;p&gt;When a project is transferred between organizations, the &lt;code&gt;OrganizationProject&lt;/code&gt; junction record is correctly deleted and recreated. However, the &lt;code&gt;TeamProjectRole&lt;/code&gt; records - which grant a team&#39;s members access to specific projects - were &lt;em&gt;not&lt;/em&gt; cleaned up during the transfer.&lt;/p&gt; &lt;p&gt;This meant that if LexCorp Organization had a &#34;release-engineers&#34; team with Owner-level access to a project, and that project was transferred to Organization OsCorp, the LexCorp team&#39;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&#39;t appear as individual collaborator entries in the UI.&lt;/p&gt; &lt;p&gt;The fix in &lt;a href=&#34;https://github.com/pypi/warehouse/pull/19749&#34;&gt;pypi/warehouse#19749&lt;/a&gt; ensures that &lt;code&gt;TeamProjectRole&lt;/code&gt; 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. I also added defensive filters in the project&#39;s ACL computation to verify that a team&#39;s organization matches the project&#39;s current organization before granting permissions, so stale records can&#39;t grant access regardless of how they&#39;re orphaned.&lt;/p&gt; &lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt; &lt;p&gt;Working with &lt;a href=&#34;https://www.trailofbits.com/&#34;&gt;Trail of Bits&lt;/a&gt; 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.&lt;/p&gt; &lt;p&gt;Beyond the 14 findings, the audit also produced proposal reviews for features I&#39;m considering (per-org Trusted Publishers, TOTP hardening, and more), as well as custom CodeQL queries to integrate into our CI/CD pipeline.&lt;/p&gt; &lt;p&gt;This audit was funded in partnership with the &lt;a href=&#34;https://www.sovereign.tech/&#34;&gt;Sovereign Tech Agency&lt;/a&gt;, which continues to support security improvements across the Open Source ecosystem.&lt;/p&gt; &lt;p&gt;My work at the Python Software Foundation is supported by &lt;a href=&#34;https://alpha-omega.dev/&#34;&gt;Alpha-Omega&lt;/a&gt;.&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2026-04-16-pypi-completes-second-audit/</link> <pubDate>Thu, 16 Apr 2026 19:00:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2026-04-16-pypi-completes-second-audit/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2026-04-16-pypi-completes-second-audit.png" type="image/png" length="59942" /> </item> <item> <title>Incident Report: LiteLLM/Telnyx supply-chain attacks, with guidance</title> <author>Seth Larson</author> <author>Mike Fiedler</author> <category>security</category> <category>transparency</category> <description>&lt;p&gt;This post will drill deeper into two recent supply chain exploits, targeting users of popular PyPI packages - &lt;code&gt;litellm&lt;/code&gt; &amp;amp; &lt;code&gt;telnyx&lt;/code&gt;. We also provide Python developers and maintainers with guidance on what they can do to prepare and protect themselves from future incidents.&lt;/p&gt; &lt;!-- more --&gt; &lt;h2 id=&#34;what-happened-with-litellm-and-telnyx&#34;&gt;What happened with LiteLLM and telnyx?&lt;/h2&gt; &lt;p&gt;After an API token exposure from an &lt;a href=&#34;https://www.aquasec.com/blog/trivy-supply-chain-attack-what-you-need-to-know/&#34;&gt;exploited Trivy dependency&lt;/a&gt; releases of the packages &lt;a href=&#34;https://pypi.org/project/litellm/&#34;&gt;&lt;code&gt;litellm&lt;/code&gt;&lt;/a&gt; and &lt;a href=&#34;https://pypi.org/project/telnyx/&#34;&gt;&lt;code&gt;telnyx&lt;/code&gt;&lt;/a&gt; were published to PyPI containing credential harvesting malware.&lt;/p&gt; &lt;p&gt;The malware ran on install, harvesting sensitive credentials and files, and exfiltrating to a remote API. More details published in &lt;a href=&#34;https://osv.dev/vulnerability/PYSEC-2026-2&#34;&gt;the advisory for LiteLLM (PYSEC-2026-2)&lt;/a&gt;, the &lt;a href=&#34;https://docs.litellm.ai/blog/security-update-march-2026&#34;&gt;LiteLLM blog post&lt;/a&gt; about the incident, &lt;a href=&#34;https://osv.dev/vulnerability/PYSEC-2026-3&#34;&gt;the advisory for telnyx (PYSEC-2026-3)&lt;/a&gt; and the &lt;a href=&#34;https://telnyx.com/resources/telnyx-python-sdk-supply-chain-security-notice-march-2026&#34;&gt;telnyx notice&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;After contacting the &lt;code&gt;litellm&lt;/code&gt; and &lt;code&gt;telnyx&lt;/code&gt; maintainers, Mike and Seth collaborated with each team on steps forward, including token rotation, release removals, and recommendations around further security practices like using &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/&#34;&gt;Trusted Publishers&lt;/a&gt; which both projects have since adopted.&lt;/p&gt; &lt;h2 id=&#34;why-is-this-malware-different&#34;&gt;Why is this malware different?&lt;/h2&gt; &lt;p&gt;This class of malware is different from most malware published to PyPI, which are mostly published as &lt;strong&gt;new packages&lt;/strong&gt;, either as typosquats or with a plan to share the package with others and hope they install it. This malware is &#34;injected&#34; into open source packages that are &lt;strong&gt;already in widespread use&lt;/strong&gt;. The malware injection occurs one of two ways:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Targeting open source projects with insecure repositories, release workflows, or authentication&lt;/li&gt; &lt;li&gt;Targeting developers installing the latest versions of open source projects and exfiltrating API tokens and keys&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Using the API tokens and keys gathered from developer machines, the malware authors are able to further compromise other open source packages if API tokens for PyPI or GitHub are exfiltrated. This cycle continues as long as it is effective in exfiltrating more credentials. &lt;/p&gt; &lt;h2 id=&#34;what-is-pypi-doing-to-mitigate-malware&#34;&gt;What is PyPI doing to mitigate malware?&lt;/h2&gt; &lt;p&gt;With daily volume of &lt;strong&gt;~700-800 new projects&lt;/strong&gt; created daily on PyPI, this poses a scaling challenge. PyPI partners with security researchers from our community who regularly scan and report malware through elevated channels to facilitate quicker remediation times.&lt;/p&gt; &lt;p&gt;Below see the timeline of events.&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;LiteLLM download timeline with reports and quarantine&#34; src=&#34;../../assets/2026-04-02-incident-report-litellm-telnyx-supply-chain-attack/litellm_downloads_timeline.png&#34; /&gt;&lt;/p&gt; &lt;p&gt;During the window of attack, the exploited versions of &lt;code&gt;litellm&lt;/code&gt; were downloaded over 119k times.&lt;/p&gt; &lt;p&gt;PyPI received 13 inbound reports from concerned users, leveraging the &lt;a href=&#34;../2024-03-06-malware-reporting-evolved/&#34;&gt;&#34;Report project as malware&#34; feature&lt;/a&gt;, added back in 2024, accelerating the review/action.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;From upload to first report: 1h 19m&lt;/li&gt; &lt;li&gt;First report to quarantine: 1h 12m&lt;/li&gt; &lt;li&gt;From upload to quarantine (total exposure time): &lt;strong&gt;2h 32m&lt;/strong&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;LiteLLM is typically installed &lt;a href=&#34;https://pepy.tech/projects/litellm?timeRange=threeMonths&amp;amp;category=version&amp;amp;includeCIDownloads=true&amp;amp;granularity=weekly&amp;amp;viewType=line&amp;amp;versions=*&#34;&gt;~15-20 million times per week&lt;/a&gt;. Averaging this out to an &#34;installs per minute&#34; rate nets a value between &lt;strong&gt;~1700 installs per minute&lt;/strong&gt;. This means between &lt;strong&gt;~40-50% of all installs&lt;/strong&gt; of LiteLLM were unpinned and fetching the latest version on each install invocation.&lt;/p&gt; &lt;p&gt;Because these systems are pulling latest, this leaves very little time for researchers and PyPI admins to report, triage, and quarantine malware when published to a popular package. Read on for information on &#34;&lt;a href=&#34;#dependency-cooldowns&#34;&gt;dependency cooldowns&lt;/a&gt;&#34;&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Telnyx download timeline with reports and quarantine&#34; src=&#34;../../assets/2026-04-02-incident-report-litellm-telnyx-supply-chain-attack/telnyx_downloads_timeline.png&#34; /&gt;&lt;/p&gt; &lt;p&gt;PyPI&#39;s remediation response to &lt;code&gt;telnyx&lt;/code&gt; was autonomously taken thanks to our pool of trusted reporters. These reporters add weight to any given report, triggering an automated quarantine feature. Subscribe to the PyPI blog for a future update about our automatic quarantining system.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;From upload to first report: 1h 45m&lt;/li&gt; &lt;li&gt;First report to quarantine: 1h 57m&lt;/li&gt; &lt;li&gt;Form upload to quarantine (total exposure time): 3h 42m&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Below are a few methods to make your usage of Python packages from PyPI more secure and to avoid installing malware.&lt;/p&gt; &lt;h2 id=&#34;protecting-yourself-as-a-developer&#34;&gt;Protecting yourself as a developer&lt;/h2&gt; &lt;h3 id=&#34;dependency-cooldowns&#34;&gt;Dependency Cooldowns&lt;/h3&gt; &lt;p&gt;One method to avoid &#34;drinking from the firehose&#34; and allow time for malware to be detected and remediated is using &#34;Dependency Cooldowns&#34;. Dependency cooldowns is a strategy for package installers (like &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, etc) to avoid installing packages that have very recently been published to PyPI. By doing so, this allows for human operators like security researchers and PyPI admins a chance to respond to reports of malware.&lt;/p&gt; &lt;p&gt;Dependency cooldowns work best when they are configured &#34;globally&#34; on a developer machine and then passively protect developers from compromises on every invocation of pip or uv. Setting a relative value like &#34;3 days&#34; (&#34;&lt;code&gt;P3D&lt;/code&gt;&#34; per RFC 3339) means packages that are newer than 3 days will not be installed.&lt;/p&gt; &lt;p&gt;uv already supports &lt;a href=&#34;https://docs.astral.sh/uv/concepts/resolution/#dependency-cooldowns&#34;&gt;setting relative dependency cooldown&lt;/a&gt; via &lt;code&gt;--exclude-newer&lt;/code&gt;. You can &lt;a href=&#34;https://docs.astral.sh/uv/concepts/configuration-files/&#34;&gt;configure the option globally&lt;/a&gt; (in &lt;code&gt;~/.config/uv/uv.toml&lt;/code&gt;) or per-project in your &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-toml&#34;&gt;[tool.uv] exclude-newer = &#34;P3D&#34; # &#34;3 days&#34; in RFC 3339 format&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Relative dependency cooldowns are coming soon to pip v26.1 which &lt;a href=&#34;https://pip.pypa.io/en/stable/development/release-process/&#34;&gt;should be available in April&lt;/a&gt; of this year. Once they are available you can set the option in &lt;a href=&#34;https://pip.pypa.io/en/stable/topics/configuration/&#34;&gt;your &lt;code&gt;pip.conf&lt;/code&gt; file&lt;/a&gt; (&lt;code&gt;~/.config/pip/pip.conf&lt;/code&gt;):&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-ini&#34;&gt;[install] uploaded-prior-to = P3D&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;a href=&#34;https://ichard26.github.io/blog/2026/01/whats-new-in-pip-26.0/#excluding-distributions-by-upload-time&#34;&gt;Starting in pip v26.0&lt;/a&gt; you can set absolute dependency cooldowns with pip from the command-line, likely paired with another tool like &lt;code&gt;date&lt;/code&gt; to calculate an absolute date from a relative offset like &#34;&lt;code&gt;3 days&lt;/code&gt;&#34;:&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-shell&#34;&gt;python -m pip install \ --uploaded-prior-to=$(date -d &#39;-3days&#39; -Idate) \ simplepackage&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Applying dependency cooldowns everywhere isn&#39;t a silver bullet, though! There are certain situations where you &lt;em&gt;do&lt;/em&gt; want the latest version of a package as soon as possible, like when applying patches for vulnerabilities. Dependency cooldowns should be paired with a vulnerability scanning strategy so security updates for your application&#39;s dependencies aren&#39;t waiting to be deployed. For example, Dependabot and Renovate both bypass dependency cooldowns by default for security updates.&lt;/p&gt; &lt;p&gt;You can manually bypass a dependency cooldown in pip and uv by setting a value of &#34;the current day&#34; to get the actual latest release:&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-shell&#34;&gt;python -m pip install \ --uploaded-prior-to=P0D \ simplepackage==26.3.31&lt;/code&gt;&lt;/pre&gt; &lt;h3 id=&#34;locking-dependencies&#34;&gt;Locking Dependencies&lt;/h3&gt; &lt;p&gt;Installing a package from PyPI without a &#34;lock&#34; means that it&#39;s possible to receive new code &lt;em&gt;every time you run pip install&lt;/em&gt;. This leaves the door open for a compromise of a package to immediately get installed and execute malware on the installing systems.&lt;/p&gt; &lt;p&gt;If you&#39;re a developer of an application using PyPI packages you should be using lock files both for security and reproducibility of your application. Some examples of tools which produce lock files for applications are:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;uv lock&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;pip-compile --generate-hashes&lt;/code&gt;&lt;/li&gt; &lt;li&gt;&lt;code&gt;pipenv&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Note that &lt;code&gt;pip freeze&lt;/code&gt; doesn&#39;t create a lock file: a lock file must include checksums / hashes of the package archives to be secure and reproducible. &lt;code&gt;pip freeze&lt;/code&gt; only records packages and their versions. pip is working on experimental support for the &lt;code&gt;pylock.toml&lt;/code&gt; standard through the &lt;a href=&#34;https://pip.pypa.io/en/stable/cli/pip_lock&#34;&gt;&lt;code&gt;pip lock&lt;/code&gt; sub-command&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&#34;protecting-your-project-as-an-open-source-maintainer&#34;&gt;Protecting your project as an open source maintainer&lt;/h2&gt; &lt;p&gt;If you are a maintainer of an open source project on PyPI, you can do your part to protect your users from compromises. There are three approaches we recommend: &lt;/p&gt; &lt;ol&gt; &lt;li&gt;securing your release workflows&lt;/li&gt; &lt;li&gt;using Trusted Publishers&lt;/li&gt; &lt;li&gt;adding 2FA to all accounts associated with open source development&lt;/li&gt; &lt;/ol&gt; &lt;h3 id=&#34;securing-release-workflows&#34;&gt;Securing release workflows&lt;/h3&gt; &lt;p&gt;If you&#39;re using a continuous deployment system to publish packages to Python: these workflows are targets for attackers. You can prevent most of the danger by applying a handful of security recommendations:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;Avoid insecure triggers.&lt;/strong&gt; Workflows that can be triggered by an attacker, especially with inputs they control (such as PR titles, branch titles) have been used in the past to inject commands. The trigger &lt;code&gt;pull_request_target&lt;/code&gt; from GitHub Actions in particular is difficult to use securely and should be avoided.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Sanitize parameters and inputs.&lt;/strong&gt; Any workflow parameter or input that can expand into an executed command carries potential to be used by attackers. Sanitize values by passing them as environment variables to commands to avoid template injection attacks.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Avoid mutable references.&lt;/strong&gt; Lock or pin your dependencies in workflows. Favor using Git commit SHAs instead of Git tags, as tags are writeable. Maintain a lock file for PyPI dependencies used in workflows.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Use reviewable deployments.&lt;/strong&gt; Trusted Publishers for GitHub supports &#34;GitHub Environments&#34; as a required step. This makes publishing your package to PyPI require a review from your GitHub account, meaning a higher bar for an attacker to compromise.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;If you are using GitHub Actions as your continuous deployment provider, we highly recommend the tool &#34;&lt;a href=&#34;https://github.com/zizmorcore/zizmor/&#34;&gt;Zizmor&lt;/a&gt;&#34; for detecting and fixing insecure workflows.&lt;/p&gt; &lt;h3 id=&#34;trusted-publishers-over-api-tokens&#34;&gt;Trusted Publishers over API tokens&lt;/h3&gt; &lt;p&gt;If the platform you use to publish to PyPI supports &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/&#34;&gt;Trusted Publishers&lt;/a&gt; (GitHub, GitLab, Google Cloud Build, ActiveState) then you should use Trusted Publishers instead of API tokens.&lt;/p&gt; &lt;p&gt;PyPI API tokens are &#34;long-lived&#34;, meaning if they are exfiltrated by an attacker that attacker can use the token at a much later date even if you don&#39;t detect the initial compromise. Trusted Publishers comparatively uses short-lived tokens, meaning they need to be used immediately and don&#39;t require manual &#34;rotating&#34; in the case of compromise.&lt;/p&gt; &lt;p&gt;Trusted Publishers also provides a valuable signal to downstream users through &lt;a href=&#34;https://docs.pypi.org/attestations/&#34;&gt;Digital Attestations&lt;/a&gt;. This means users can detect when a release &lt;em&gt;hasn&#39;t&lt;/em&gt; been published using the typical release workflow, likely drawing more scrutiny.&lt;/p&gt; &lt;h3 id=&#34;adding-2fa-to-open-source-development-accounts&#34;&gt;Adding 2FA to open source development accounts&lt;/h3&gt; &lt;p&gt;We may be starting to sound like a broken record, but 2FA should be used for all accounts associated with open source development: not just PyPI. Think about accounts like version control / software forges (GitHub, GitLab, Codeberg, Forgejo) and your email provider. &lt;a href=&#34;../2024-01-01-2fa-enforced/&#34;&gt;PyPI has required 2FA to be enabled to publish packages&lt;/a&gt; since the beginning of 2024, but enabling phishing-resistant 2FA like a hardware key can protect you further.&lt;/p&gt; &lt;h2 id=&#34;how-can-you-support-this-kind-of-work&#34;&gt;How can you support this kind of work?&lt;/h2&gt; &lt;p&gt;Security work isn&#39;t free. You can support security work on the Python Package Index by supporting the Python Software Foundation (PSF). If you or your organization is interested in sponsoring or donating to the PSF so we can continue supporting Python, PyPI, and its community, check out the &lt;a href=&#34;https://www.python.org/sponsors/application/&#34;&gt;PSF&#39;s sponsorship program&lt;/a&gt;, &lt;a href=&#34;https://www.python.org/psf/donations/&#34;&gt;donate directly&lt;/a&gt;, or contact our team at &lt;a href=&#34;mailto:sponsors@python.org&#34;&gt;sponsors@python.org&lt;/a&gt;!&lt;/p&gt; &lt;p&gt;Mike Fiedler and Seth Larson&#39;s roles as &lt;a href=&#34;https://www.python.org/psf/developersinresidence/&#34;&gt;PyPI Safety &amp;amp; Security Engineer and Security Developer-in-Residence&lt;/a&gt; at the Python Software Foundation are supported by &lt;a href=&#34;https://alpha-omega.dev&#34;&gt;Alpha Omega&lt;/a&gt;.&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2026-04-02-incident-report-litellm-telnyx-supply-chain-attack/</link> <pubDate>Thu, 02 Apr 2026 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2026-04-02-incident-report-litellm-telnyx-supply-chain-attack/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2026-04-02-incident-report-litellm-telnyx-supply-chain-attack.png" type="image/png" length="70357" /> </item> <item> <title>Dispatch from PyPI Land: A Year (and a Half!) as the Inaugural PyPI Support Specialist</title> <author>Maria Ashna</author> <category>support</category> <description>&lt;p&gt;Hello there! I am Maria, the inaugural PyPI Support Specialist. I go by &#34;Thespi-Brain&#34; on GitHub. I wanted to provide a dispatch of how this past year (and a half!) has been regarding my role and PyPI. PyPI has now reached over a million users and has over 700,000 projects. It is, without a doubt, a critical part of the Python ecosystem. As the inaugural PyPI Support Specialist, there were numerous challenges that needed to be tackled regarding PyPI support, such as the ever growing backlog of account recovery and PEP 541 issues. &lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;Prior to stepping into this role, the account recovery backlog was four months behind. The PEP 541 backlog was a year and four months behind. There was also a lack of documentation around PyPI Support procedures. I started my role at PSF in July 2024. I am happy to report some highlights of my tenure so far:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Account Recovery backlog is now current with average triage time of 1.5 days&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt; &lt;li&gt;PEP 541 backlog is current as of December 2025, with an average first triage/issue response time of 1 week&lt;sup id=&#34;fnref2:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt; &lt;li&gt;Between July 2024 and December 2024, &lt;strong&gt;725&lt;/strong&gt; of account recovery issues were resolved&lt;/li&gt; &lt;li&gt;Between Jan 2025 and December 2025, &lt;strong&gt;2221&lt;/strong&gt; account recovery issues were resolved&lt;/li&gt; &lt;li&gt;Between Jan 2025 and December 2025, over &lt;strong&gt;~500&lt;/strong&gt; PEP 541 issues were resolved&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Since there was no documentation prior to this role, support processes and procedures have been internally documented with an end user friendly version coming out soon! &lt;/p&gt; &lt;p&gt;With account recovery and PEP 541 backlog under control, this also provided an opportunity to focus on getting PyPI Orgs back up again after its introduction in 2023. As of today, over 8000 applications have been approved with both community and company organizations combined. &lt;/p&gt; &lt;p&gt;In addition to the above, I have also created several feature requests for the Warehouse side of things to facilitate better workflow and tooling, particularly with PyPI Org application processing as well as feature suggestions to better process PEP 541 issues that are related to ultranormalized names. &lt;/p&gt; &lt;p&gt;A lot has been accomplished in the first year (and a half!), but of course, there is always more work to be done to ensure that PyPI is running smoothly. Here’s to another year (and a half!) of more exciting things to come!&lt;/p&gt; &lt;div class=&#34;footnote&#34;&gt; &lt;hr /&gt; &lt;ol&gt; &lt;li id=&#34;fn:1&#34;&gt; &lt;p&gt;Exceptions to this occur during holidays, conference times and other times staff may be out.&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; title=&#34;Jump back to footnote 1 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref2:1&#34; title=&#34;Jump back to footnote 1 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;/div&gt;</description> <link>https://blog.pypi.org/posts/2026-01-26-a-year-and-a-half-as-inaugural-pypi-support-specialist/</link> <pubDate>Mon, 26 Jan 2026 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2026-01-26-a-year-and-a-half-as-inaugural-pypi-support-specialist/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2026-01-26-a-year-and-a-half-as-inaugural-pypi-support-specialist.png" type="image/png" length="71677" /> </item> <item> <title>PyPI in 2025: A Year in Review</title> <author>Dustin Ingram</author> <category>new features</category> <category>organizations</category> <category>security</category> <description>&lt;p&gt;As 2025 comes to a close, it&#39;s time to look back at another busy year for the Python Package Index. This year, we&#39;ve focused on delivering critical security enhancements, rolling out powerful new features for organizations, improving the overall user experience for the millions of developers who rely on PyPI every day, and responding to a number of security incidents with transparency.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;But first, let&#39;s look at some numbers that illustrate the sheer scale of PyPI in 2025:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;More than &lt;strong&gt;3.9 million&lt;/strong&gt; new files published&lt;/li&gt; &lt;li&gt;More than &lt;strong&gt;130,000&lt;/strong&gt; new projects created&lt;/li&gt; &lt;li&gt;&lt;strong&gt;1.92 exabytes&lt;/strong&gt; of total data transferred&lt;/li&gt; &lt;li&gt;&lt;strong&gt;2.56 trillion&lt;/strong&gt; total requests served&lt;/li&gt; &lt;li&gt;&lt;strong&gt;81,000&lt;/strong&gt; requests per second on average&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;These numbers are a testament to the continued growth and vibrancy of the Python community.&lt;/p&gt; &lt;p&gt;Let&#39;s dive into some of the key improvements we&#39;ve made to PyPI this year.&lt;/p&gt; &lt;h2 id=&#34;security-first-security-always&#34;&gt;Security First, Security Always&lt;/h2&gt; &lt;p&gt;Security is our top priority, and in 2025 we&#39;ve shipped a number of features to make PyPI more secure than ever.&lt;/p&gt; &lt;h3 id=&#34;enhanced-two-factor-authentication-2fa-for-phishing-resistance&#34;&gt;Enhanced Two-Factor Authentication (2FA) for Phishing Resistance&lt;/h3&gt; &lt;p&gt;We&#39;ve made significant improvements to our 2FA implementation, starting with &lt;strong&gt;&lt;a href=&#34;../2025-11-14-login-verification/&#34;&gt;email verification for TOTP-based logins&lt;/a&gt;&lt;/strong&gt;. This adds an extra layer of security to your account by requiring you to confirm your login from a trusted device, when using a phishable 2FA method like TOTP.&lt;/p&gt; &lt;p&gt;Since rolling out these changes, we&#39;ve seen:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;more than &lt;strong&gt;52%&lt;/strong&gt; of active users with non-phishable 2FA enabled&lt;/li&gt; &lt;li&gt;more than &lt;strong&gt;45,000&lt;/strong&gt; total unique verified logins&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&#34;trusted-publishing-and-attestations&#34;&gt;Trusted Publishing and Attestations&lt;/h3&gt; &lt;p&gt;&lt;strong&gt;&lt;a href=&#34;../2025-11-10-trusted-publishers-coming-to-orgs/&#34;&gt;Trusted publishing&lt;/a&gt;&lt;/strong&gt; continues to be a cornerstone of our security strategy. This year, we&#39;ve expanded support to include &lt;strong&gt;GitLab Self-Managed instances&lt;/strong&gt;, allowing maintainers to automate their release process without needing to manage long-lived API tokens. We&#39;ve also introduced support for &lt;strong&gt;custom OIDC issuers for organizations&lt;/strong&gt;, giving companies more control over their publishing pipelines.&lt;/p&gt; &lt;p&gt;Adoption of trusted publishing has been fantastic:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;more than &lt;strong&gt;50,000&lt;/strong&gt; projects are now using trusted publishing&lt;/li&gt; &lt;li&gt;more than &lt;strong&gt;20%&lt;/strong&gt; of all file uploads to PyPI in the last year were done via trusted publishers&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;We&#39;ve also been hard at work on &lt;strong&gt;attestations&lt;/strong&gt;, a security feature that allows publishers to make verifiable claims about their software. We&#39;ve added support for attestations from all Trusted Publishing providers, and we&#39;re excited to see how the community uses this feature to improve the security of the software supply chain.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;17%&lt;/strong&gt; of all uploads to PyPI in the last year included an attestation.&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&#34;proactive-security-measures&#34;&gt;Proactive Security Measures&lt;/h3&gt; &lt;p&gt;Beyond user-facing features, we&#39;ve also implemented a number of proactive security measures to protect the registry from attack. These include:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;Phishing Protection:&lt;/strong&gt; To combat the ongoing threat of phishing attacks, PyPI now &lt;a href=&#34;../2025-07-28-pypi-phishing-attack/&#34;&gt;detects and warns users about untrusted domains&lt;/a&gt;.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Improved ZIP file security:&lt;/strong&gt; We&#39;ve hardened our upload pipeline to &lt;a href=&#34;../2025-08-07-wheel-archive-confusion-attacks/&#34;&gt;prevent a class of attacks involving malicious ZIP files&lt;/a&gt;.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Typosquatting detection:&lt;/strong&gt; PyPI now automatically detects and flags potential typosquatting attempts during project creation.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Domain Resurrection Prevention:&lt;/strong&gt; We now periodically check for expired domains to &lt;a href=&#34;../2025-08-18-preventing-domain-resurrections/&#34;&gt;prevent domain resurrection attacks&lt;/a&gt;.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Spam Prevention&lt;/strong&gt;: We&#39;ve taken action against spam campaigns, including &lt;a href=&#34;../2025-06-15-prohibiting-inbox-ru-emails/&#34;&gt;prohibiting registrations from specific domains&lt;/a&gt; that were a source of abuse.&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&#34;transparency-and-incident-response&#34;&gt;Transparency and Incident Response&lt;/h2&gt; &lt;p&gt;This year, we&#39;ve also focused on providing transparent and timely information about security incidents affecting PyPI. We&#39;ve published detailed incident reports on a number of events, including:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;An issue with &lt;a href=&#34;../2025-04-14-incident-report-organization-team-privileges/&#34;&gt;privileges persisting in organization teams&lt;/a&gt;.&lt;/li&gt; &lt;li&gt;A widespread &lt;a href=&#34;../2025-07-31-incident-report-phishing-attack/&#34;&gt;phishing attack targeting PyPI users&lt;/a&gt;.&lt;/li&gt; &lt;li&gt;A &lt;a href=&#34;../2025-09-16-github-actions-token-exfiltration/&#34;&gt;token exfiltration campaign via GitHub Actions workflows&lt;/a&gt;.&lt;/li&gt; &lt;li&gt;The potential implications of the &lt;a href=&#34;../2025-11-26-pypi-and-shai-hulud/&#34;&gt;&#34;Shai-Hulud&#34; attack on the npm ecosystem&lt;/a&gt;.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;We believe that transparency is key to building and maintaining trust with our community, and we&#39;ll continue to provide these reports as needed.&lt;/p&gt; &lt;h2 id=&#34;safety-and-support-requests&#34;&gt;Safety and Support Requests&lt;/h2&gt; &lt;p&gt;This year, our safety &amp;amp; support team and administrators have been working diligently to address user requests and combat malware to maintain a healthy ecosystem. We&#39;re proud to report significant progress in handling various types of support inquiries and improving our malware response.&lt;/p&gt; &lt;h3 id=&#34;malware-response&#34;&gt;Malware Response&lt;/h3&gt; &lt;p&gt;We&#39;ve continued to improve our malware detection and response capabilities. This year, we&#39;ve processed &lt;strong&gt;more than 2000 malware reports&lt;/strong&gt;. This is a testament to the vigilance of our community and the dedication of our administrators.&lt;/p&gt; &lt;p&gt;Our goal is to reduce the time it takes to remove malware from PyPI, and we&#39;re happy to report that we&#39;re making significant progress: in the last year, 66% of all reports were handled within 4 hours, climbing to 92% within 24 hours, with only a few more complex issues reaching the maximum of 4 days to remediate.&lt;/p&gt; &lt;h3 id=&#34;support-requests&#34;&gt;Support Requests&lt;/h3&gt; &lt;p&gt;Our support team has also been hard at work making sure our users can continue to be effective on PyPI. This year, we&#39;ve successfully resolved &lt;strong&gt;2221&lt;/strong&gt; individual account recovery requests.&lt;/p&gt; &lt;p&gt;We&#39;ve also handled more than 500 project name retention requests (PEP 541). This includes an average first triage time of less than 1 week. This is a significant improvement compared to the previous 9-month backlog, and we&#39;re happy to report that the backlog is current for the month of December.&lt;/p&gt; &lt;h2 id=&#34;organizations-growth&#34;&gt;Organizations Growth&lt;/h2&gt; &lt;p&gt;One of our biggest announcements in previous years was the general availability of &lt;strong&gt;organizations&lt;/strong&gt; on PyPI. Organizations provide a way for companies and community projects to manage their packages, teams, and billing in a centralized location.&lt;/p&gt; &lt;p&gt;We have continued to see growing usage of organizations:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;7,742&lt;/strong&gt; organizations have been created on PyPI&lt;/li&gt; &lt;li&gt;&lt;strong&gt;9,059&lt;/strong&gt; projects are now managed by organizations&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;We&#39;ve been hard at work adding new features to organizations, including team management, project transfers, and a comprehensive admin interface. We&#39;re excited to see organizations use these features to use PyPI more effectively.&lt;/p&gt; &lt;h2 id=&#34;a-better-pypi-for-everyone&#34;&gt;A Better PyPI for Everyone&lt;/h2&gt; &lt;p&gt;Finally, we&#39;ve made a number of improvements to the overall maintainer experience on PyPI. These include:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;Project Lifecycle Management:&lt;/strong&gt; You can now &lt;a href=&#34;../2025-01-30-archival/&#34;&gt;archive your projects&lt;/a&gt; to signal that they are no longer actively maintained. This is part of a larger effort to standardize &lt;a href=&#34;../2025-08-14-project-status-markers/&#34;&gt;project status markers as proposed in PEP 792&lt;/a&gt;.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;New Terms of Service:&lt;/strong&gt; We&#39;ve introduced a new &lt;a href=&#34;../2025-02-25-terms-of-service/&#34;&gt;Terms of Service&lt;/a&gt; to formalize our policies and enable new features like organizations.&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&#34;looking-ahead-to-2026&#34;&gt;Looking Ahead to 2026&lt;/h2&gt; &lt;p&gt;We&#39;re proud of the progress we&#39;ve made in 2025, but we know there&#39;s always more to do. In 2026, we&#39;ll continue to focus on improving the security, stability, and usability of PyPI for the entire Python community.&lt;/p&gt; &lt;h2 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h2&gt; &lt;p&gt;As always, a huge thanks to our &lt;a href=&#34;https://pypi.org/sponsors/&#34;&gt;sponsors&lt;/a&gt; who make the scale and reliability of PyPI possible, and a special shout-out to Fastly for being a critical infrastructure donor.&lt;/p&gt; &lt;p&gt;We&#39;d also like to extend a special thank you to a few individuals who made significant contributions to PyPI this year. Thank you to &lt;strong&gt;William Woodruff&lt;/strong&gt;, &lt;strong&gt;Facundo Tuesca&lt;/strong&gt;, and &lt;strong&gt;Seth Michael Larson&lt;/strong&gt; for your work on trusted publishing, attestations, project archival, zipfile mitigation, and other security features.&lt;/p&gt; &lt;p&gt;Finally, PyPI wouldn&#39;t be what it is today without the countless hours of work from our community. A huge thank you to everyone who contributed code, opened an issue, or provided feedback this year. As always, we&#39;re grateful for the contributions of our community, whether it&#39;s through code, documentation, or feedback. PyPI wouldn&#39;t be what it is today without you.&lt;/p&gt; &lt;p&gt;Here&#39;s to a great 2026!&lt;/p&gt; &lt;p&gt;-- &lt;em&gt;Dustin Ingram, on behalf of the PyPI team.&lt;/em&gt;&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2025-12-31-pypi-2025-in-review/</link> <pubDate>Wed, 31 Dec 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-12-31-pypi-2025-in-review/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-12-31-pypi-2025-in-review.png" type="image/png" length="56650" /> </item> <item> <title>PyPI and Shai-Hulud: Staying Secure Amid Emerging Threats</title> <author>Mike Fiedler</author> <category>publishing</category> <category>security</category> <description>&lt;p&gt;An attack on the npm ecosystem continues to evolve, exploiting compromised accounts to publish malicious packages. This campaign, dubbed &lt;em&gt;Shai-Hulud&lt;/em&gt;, has targeted large volumes of packages in the JavaScript ecosystem, exfiltrating credentials to further propagate itself.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;PyPI has not been exploited&lt;/strong&gt;, however some PyPI credentials were found exposed in compromised repositories. We&#39;ve revoked these tokens as a precaution, there&#39;s no evidence they have been used maliciously. This post raises awareness about the attack and encourages proactive steps to secure your accounts, especially if you&#39;re using build platforms to publish packages to PyPI.&lt;/p&gt; &lt;!-- more --&gt; &lt;h2 id=&#34;how-does-this-relate-to-pypi&#34;&gt;How does this relate to PyPI?&lt;/h2&gt; &lt;p&gt;This week, a security researcher disclosed long-lived PyPI credentials exposed as part of the Shai-Hulud campaign. The credentials were found in GitHub repositories (stored as repository secrets), and were still valid. We saw an attack with insecure workflow settings for &lt;a href=&#34;../2024-12-11-ultralytics-attack-analysis/&#34;&gt;Ultralytics in 2024&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;While the campaign primarily targets npm, some projects use &lt;a href=&#34;https://en.wikipedia.org/wiki/Monorepo&#34;&gt;monorepo&lt;/a&gt; setups, publishing both JavaScript packages to npmjs.com and Python packages to PyPI from the same repository. When attackers compromise these repositories, they can extract credentials for multiple platforms.&lt;/p&gt; &lt;p&gt;We investigated the reported credentials and found they were associated with accounts that hadn&#39;t published recently. We&#39;ve revoked these credentials and reached out to affected users to advise them to rotate any remaining tokens.&lt;/p&gt; &lt;h2 id=&#34;what-can-i-do-to-protect-my-pypi-account&#34;&gt;What can I do to protect my PyPI account?&lt;/h2&gt; &lt;p&gt;Here are security practices to protect your PyPI account:&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;Use Trusted Publishing:&lt;/strong&gt; If you are using a build platform to publish packages to PyPI, consider using a &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/&#34;&gt;Trusted Publisher&lt;/a&gt;. This eliminates the need to manage long-lived authentication tokens, reducing the risk of credential exposure. Trusted Publishing uses short-lived, scoped tokens for each build, minimizing the impact of any potential compromise. This approach has &lt;a href=&#34;../2025-11-10-trusted-publishers-coming-to-orgs/&#34;&gt;risen in popularity&lt;/a&gt;, with other registries like &lt;a href=&#34;https://crates.io/docs/trusted-publishing&#34;&gt;Crates.io&lt;/a&gt;, &lt;a href=&#34;https://guides.rubygems.org/trusted-publishing/&#34;&gt;RubyGems&lt;/a&gt;, and &lt;a href=&#34;https://docs.npmjs.com/trusted-publishers&#34;&gt;npmjs.com&lt;/a&gt; adopting similar models.&lt;/p&gt; &lt;p&gt;When using GitHub Actions, consider layering in additional security measures, like requiring human approval via &lt;a href=&#34;https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments&#34;&gt;GitHub Environments&lt;/a&gt; before publishing. &lt;a href=&#34;https://www.pyopensci.org/blog/python-packaging-security-publish-pypi.html&#34;&gt;This blog post from pyOpenSci&lt;/a&gt; has detailed guidance on adding manual review steps to GitHub Actions workflows.&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;Audit your workflows for misconfiguration:&lt;/strong&gt; Review your GitHub Actions workflows for any potential security issues. Tools like &lt;a href=&#34;https://docs.zizmor.sh/&#34;&gt;zizmor&lt;/a&gt; and &lt;a href=&#34;https://codeql.github.com/&#34;&gt;CodeQL&lt;/a&gt; can help identify vulnerabilities in your CI/CD pipelines. Adopt scanning as automated actions for the repository to catch future issues.&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;strong&gt;Review your account activity:&lt;/strong&gt; Regularly check your PyPI account activity for any unauthorized actions. If you notice any suspicious activity, &lt;a href=&#34;https://pypi.org/security/&#34;&gt;report it to the PyPI security team&lt;/a&gt; immediately.&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Taking any of these steps helps mitigate the risk of compromise and keeps packages secure.&lt;/p&gt; &lt;h2 id=&#34;references&#34;&gt;References&lt;/h2&gt; &lt;p&gt;Some blog posts covering the attack behaviors and mitigation steps:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;a href=&#34;https://www.aikido.dev/blog/shai-hulud-strikes-again-hitting-zapier-ensdomains&#34;&gt;Aikido&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem&#34;&gt;CISA&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://securitylabs.datadoghq.com/articles/shai-hulud-2.0-npm-worm/&#34;&gt;Datadog&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://helixguard.ai/blog/malicious-sha1hulud-2025-11-24&#34;&gt;HelixGuard&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://www.reversinglabs.com/blog/shai-hulud-worm-npm&#34;&gt;ReversingLabs&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://socket.dev/blog/shai-hulud-strikes-again-v2&#34;&gt;Socket&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://www.stepsecurity.io/blog/sha1-hulud-the-second-coming-zapier-ens-domains-and-other-prominent-npm-packages-compromised&#34;&gt;StepSecurity&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://www.wiz.io/blog/shai-hulud-2-0-ongoing-supply-chain-attack&#34;&gt;Wiz&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt;</description> <link>https://blog.pypi.org/posts/2025-11-26-pypi-and-shai-hulud/</link> <pubDate>Wed, 26 Nov 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-11-26-pypi-and-shai-hulud/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-11-26-pypi-and-shai-hulud.png" type="image/png" length="68521" /> </item> <item> <title>New Login Verification for TOTP-based Logins</title> <author>Dustin Ingram</author> <category>2fa</category> <category>security</category> <description>&lt;p&gt;We&#39;ve implemented a new security feature designed to protect PyPI users from phishing attacks: &lt;strong&gt;email verification for TOTP-based logins from new devices.&lt;/strong&gt;&lt;/p&gt; &lt;!-- more --&gt; &lt;h3 id=&#34;whats-changing&#34;&gt;What&#39;s Changing?&lt;/h3&gt; &lt;p&gt;Previously, when logging in with a Time-based One-Time Password (TOTP) authenticator, a successful TOTP code was sufficient. Now, if you log in from a device or browser that PyPI doesn&#39;t recognize, we will send a verification email to the email address associated with your PyPI account with the subject &#34;Unrecognized login to your PyPI account&#34;. You will need to click a link in this email to confirm the login attempt before you can proceed, after which the current device will be trusted for future logins.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Users who have enabled WebAuthn (security keys) or passkeys for 2FA will not see any changes&lt;/strong&gt;, as these methods are inherently phishing-resistant. They cryptographically bind the authentication to the specific website (origin), meaning an attacker cannot trick you into authenticating on a fake site, unlike TOTP codes which can be phished.&lt;/p&gt; &lt;h3 id=&#34;why-this-change&#34;&gt;Why This Change?&lt;/h3&gt; &lt;p&gt;This extra step significantly enhances PyPI user account security by mitigating the risk of phishing. As we&#39;ve discussed in previous posts, such as &lt;a href=&#34;https://blog.pypi.org/posts/2025-09-23-plenty-of-phish-in-the-sea/&#34;&gt;&#34;Phishing attacks with new domains likely to continue&#34;&lt;/a&gt;, recent phishing campaigns have targeted PyPI users. This new feature is a direct response to these ongoing threats. Even if an attacker manages to steal your username, password, and a TOTP code, they won&#39;t be able to access your account from a new device without also gaining access to your email. This makes it much harder for malicious actors to compromise your account.&lt;/p&gt; &lt;h3 id=&#34;what-you-need-to-do&#34;&gt;What You Need To Do&lt;/h3&gt; &lt;p&gt;Most users will only encounter this verification when using TOTP-based 2FA when logging in from a new device. If you receive a verification email and you &lt;em&gt;did not&lt;/em&gt; attempt to log in to PyPI, please do not click the link. Instead, change your password immediately and review your account for any suspicious activity.&lt;/p&gt; &lt;p&gt;While this new feature improves the security of TOTP, we continue to recommend migrating to stronger second-factor authentication methods such as &lt;strong&gt;passkeys&lt;/strong&gt; or &lt;strong&gt;WebAuthn (security keys)&lt;/strong&gt;. These methods offer superior protection against phishing and other sophisticated attacks. If you haven&#39;t already, please consider upgrading your 2FA method for the best possible account security on PyPI.&lt;/p&gt; &lt;p&gt;We believe this change will make PyPI an even safer place for the Python community. Thank you for your continued trust and support!&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2025-11-14-login-verification/</link> <pubDate>Fri, 14 Nov 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-11-14-login-verification/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-11-14-login-verification.png" type="image/png" length="67980" /> </item> <item> <title>Trusted Publishing is popular, now for GitLab Self-Managed and Organizations</title> <author>Mike Fiedler</author> <category>oidc</category> <category>organizations</category> <category>publishing</category> <category>security</category> <description>&lt;p&gt;Trusted Publishing has proven popular since &lt;a href=&#34;../2023-04-20-introducing-trusted-publishers/&#34;&gt;its launch in 2023&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Recap:&lt;/strong&gt; Trusted Publishing enables software build platforms to publish packages to PyPI on your behalf, eliminating the need to manage long-lived authentication tokens. After a one-time setup where you delegate publishing authority to your platform, it automatically obtains short-lived, scoped tokens for each build—no manual token management required.&lt;/p&gt; &lt;p&gt;Read the &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/security-model/&#34;&gt;Security Model&lt;/a&gt; for a deeper understanding of how Trusted Publishing works.&lt;/p&gt; &lt;!-- more --&gt; &lt;h2 id=&#34;growing-adoption-and-impact&#34;&gt;Growing Adoption and Impact&lt;/h2&gt; &lt;p&gt;Since its inception, Trusted Publishing has been adopted by communities and companies alike, with ~45,000 projects configured for Trusted Publishing on PyPI so far. In early 2024 we added the ability to track whether each file upload was done via a Trusted Publisher, enabling us to analyze its impact over time. Also in 2024, more platforms were added as detailed in our &lt;a href=&#34;../2024-04-17-expanding-trusted-publisher-support/&#34;&gt;April 2024 blog post&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Here&#39;s a chart displaying the overall count of files uploaded to PyPI each month, broken down by whether they were uploaded via Trusted Publishers or not:&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Monthly File Uploads to PyPI via Trusted Publishers vs Non-Trusted Publishers&#34; src=&#34;../../assets/2025-11-10-trusted-publishers-for-orgs/files_added_to_pypi.svg&#34; /&gt;&lt;/p&gt; &lt;p&gt;A couple of numbers for comparison:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;February 2024 - 241k files added&lt;/li&gt; &lt;li&gt;October 2025 - 377k files added&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;As this chart shows, the overall scale of files uploaded to PyPI has grown (as detailed in &lt;a href=&#34;https://pyfound.blogspot.com/2025/10/open-infrastructure-is-not-free-pypi.html&#34;&gt;this blog from the PSF Director of Infrastructure on open infrastructure growth&lt;/a&gt;) but what is great to see is that the proportional rate of Trusted Publishing use has grown as well. This can be seen by charting the percentage of files uploaded for each method over time:&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Monthly File Uploads to PyPI via Trusted Publishers vs Non-Trusted Publishers, stacked&#34; src=&#34;../../assets/2025-11-10-trusted-publishers-for-orgs/files_added_to_pypi_pct.svg&#34; /&gt;&lt;/p&gt; &lt;p&gt;Here we can see that back in February 2024, only about 10% of files were uploaded via Trusted Publishers. By October 2025, that number has grown to &lt;strong&gt;over 25% of all files uploaded to PyPI&lt;/strong&gt; in a given month.&lt;/p&gt; &lt;p&gt;Hopefully posts like this and increased visibility in how to adopt Trusted Publishing will help continue this growth trend. Having folks like &lt;a href=&#34;https://github.com/webknjaz&#34;&gt;Sviatoslav Sydorenko&lt;/a&gt; and other contributors voluntarily maintain the &lt;a href=&#34;https://github.com/marketplace/actions/pypi-publish&#34;&gt;widely-used GitHub Actions workflow for Trusted Publishing&lt;/a&gt; that makes everything that much easier has been a huge help, support them as well if you can! Using that action adds &lt;a href=&#34;../2024-11-14-pypi-now-supports-digital-attestations/&#34;&gt;Digital Attestations&lt;/a&gt; automatically. &lt;img alt=&#34;🔒&#34; class=&#34;twemoji&#34; src=&#34;https://cdn.jsdelivr.net/gh/jdecked/twemoji@16.0.1/assets/svg/1f512.svg&#34; title=&#34;:lock:&#34; /&gt;&lt;/p&gt; &lt;p&gt;I hope that when we examine these stats next year, we&#39;ll see even more growth as more organizations adopt Trusted Publishing for their package publishing workflows.&lt;/p&gt; &lt;p&gt;Which brings me to...&lt;/p&gt; &lt;h2 id=&#34;expansion-of-trusted-publishing-to-gitlab-self-managed-beta&#34;&gt;Expansion of Trusted Publishing to GitLab Self-Managed Beta&lt;/h2&gt; &lt;p&gt;Since folks have been loving Trusted Publishing, I&#39;m excited to share that it&#39;s now available in beta for GitLab Self-Managed instances. This means organizations running their own GitLab can now use Trusted Publishers to publish packages more securely, without dealing with long-lived tokens.&lt;/p&gt; &lt;p&gt;As a reminder, trusted publishing support for the public GitLab.com instance has been available since early 2024.&lt;/p&gt; &lt;p&gt;Since GitLab itself can be self-hosted, we&#39;re initially launching this feature as a beta to gather feedback and ensure a smooth experience. Organizations&#39; self-hosted instances must be manually onboarded by PyPI staff during this beta phase, while we learn more about the various configurations and setups in use.&lt;/p&gt; &lt;p&gt;In this scenario, the trust relationship is established between your GitLab Self-Managed instance and PyPI, allowing GitLab CI/CD pipelines to publish packages on behalf of your organization without the need for long-lived tokens.&lt;/p&gt; &lt;p&gt;If your organization runs your own GitLab Self-Managed instances and wish to publish packages to PyPI using Trusted Publishing, we encourage you to try out Trusted Publishing and provide feedback during this beta phase. Email &lt;a href=&#34;mailto:support+orgs@pypi.org&#34;&gt;support+orgs@pypi.org&lt;/a&gt; (or click &lt;a href=&#34;mailto:support+orgs@pypi.org?subject=PyPI%20GitLab%20Self-Managed%20Interest&amp;amp;body=Hi%20PyPI%20Support%2C%0A%0AOur%20organization%20%3CREPLACE%20WITH%20PyPI%20ORG%20NAME%3E%20is%20interested%20in%20the%20Trusted%20Publisher%20beta%20for%20GitLab%20Self-Managed%20Instances.%0A%0AOur%20GitLab%20Instance%27s%20URL%20is%3A%20https%3A%2F%2F%3CYOUR%20INSTANCE%20URL%3E%0AWe%20have%20confirmed%20that%20these%20endpoints%20are%20accessible%3A%0A%20%20*%20https%3A%2F%2F%3CYOUR%20INSTANCE%20URL%3E%2F.well-known%2Fopenid-configuration%0A%20%20*%20https%3A%2F%2F%3CYOUR%20INSTANCE%20URL%3E%2Foauth%2Fdiscovery%2Fkeys%0A%0ALooking%20forward%20to%20hearing%20back%20from%20you%20soon.&#34;&gt;this link for a pre-filled email&lt;/a&gt; that you can update with your details).&lt;/p&gt; &lt;h2 id=&#34;pending-trusted-publishers-for-organizations-feature&#34;&gt;Pending Trusted Publishers for Organizations Feature&lt;/h2&gt; &lt;p&gt;Oh, and one more thing - an important feature of PyPI Organizations is now available:&lt;/p&gt; &lt;p&gt;You can now create a &lt;strong&gt;pending&lt;/strong&gt; Trusted Publisher at the Organization level.&lt;/p&gt; &lt;p&gt;Why does this matter? Previously, a PyPI user account creating a Pending Trusted Publisher (for a project that does not yet exist) would become the Owner of the project once it&#39;s been uploaded by the Trusted Publisher. This made sense for individual users, but for Organizations, it meant that the user who &lt;strong&gt;created&lt;/strong&gt; the Pending Trusted Publisher would become the Owner of the project once it was published, and may overlook transferring ownership back to the Organization, which is less than ideal.&lt;/p&gt; &lt;p&gt;With this new feature, when creating a Pending Trusted Publisher at the Organization level, the project will be owned by the Organization itself once it&#39;s published, regardless of which user created the Pending Trusted Publisher.&lt;/p&gt; &lt;p&gt;This makes managing projects in Organizations way easier, making sure projects belong to the Organization itself and not just whoever set things up.&lt;/p&gt; &lt;p&gt;If you have a PyPI Organization and wish to create a Pending Trusted Publisher, check it out on your Organization&#39;s Trusted Publishers page, by following:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Navigate to your PyPI Organization&#39;s page (e.g., &lt;code&gt;https://pypi.org/organizations/&amp;lt;your-org-name&amp;gt;/&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;Click on the &#34;Publishing&#34; tab on the sidebar.&lt;/li&gt; &lt;li&gt;See the forms to create a Pending Trusted Publisher for your Organization.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;All other behaviors of Pending Trusted Publishers remain the same, see the &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/&#34;&gt;Trusted Publishers documentation&lt;/a&gt; for more details.&lt;/p&gt; &lt;h2 id=&#34;whats-next&#34;&gt;What&#39;s next?&lt;/h2&gt; &lt;p&gt;We&#39;re continuing to work on making PyPI publishing safer and more secure, so we&#39;ll keep watching how Trusted Publishing adoption grows. During the GitLab Self-Managed beta, we&#39;d love to hear your feedback so we can improve things based on how you actually use it.&lt;/p&gt; &lt;p&gt;Some next step ideas we have, and would love your feedback on:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Adding support for &lt;a href=&#34;https://github.com/pypi/warehouse/issues/18970&#34;&gt;GitHub Enterprise Server instances&lt;/a&gt;&lt;/li&gt; &lt;li&gt;Adding support for &lt;a href=&#34;https://github.com/pypi/warehouse/issues/11096&#34;&gt;GitHub Actions Reusable Workflows&lt;/a&gt;&lt;/li&gt; &lt;li&gt;Adding support for &lt;a href=&#34;https://github.com/pypi/warehouse/issues/18889&#34;&gt;other publishing platforms&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;For any of these, feel free to visit the related GitHub issue and add your &lt;img alt=&#34;👍&#34; class=&#34;twemoji&#34; src=&#34;https://cdn.jsdelivr.net/gh/jdecked/twemoji@16.0.1/assets/svg/1f44d.svg&#34; title=&#34;:thumbsup:&#34; /&gt; reaction to the main issue to help us sort them by interest.&lt;/p&gt; &lt;p&gt;Live long, and publish! &lt;img alt=&#34;🖖&#34; class=&#34;twemoji&#34; src=&#34;https://cdn.jsdelivr.net/gh/jdecked/twemoji@16.0.1/assets/svg/1f596.svg&#34; title=&#34;:vulcan:&#34; /&gt;&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2025-11-10-trusted-publishers-coming-to-orgs/</link> <pubDate>Mon, 10 Nov 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-11-10-trusted-publishers-coming-to-orgs/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-11-10-trusted-publishers-coming-to-orgs.png" type="image/png" length="61977" /> </item> <item> <title>Phishing attacks with new domains likely to continue</title> <author>Seth Larson</author> <category>security</category> <category>transparency</category> <description>&lt;p&gt;Unfortunately the string of phishing attacks using domain-confusion and legitimate-looking emails continues. This is the &lt;a href=&#34;../2025-07-28-pypi-phishing-attack/&#34;&gt;same attack PyPI saw a few months ago&lt;/a&gt; and targeting many other open source repositories but with a different domain name. Judging from this, we believe this type of campaign will continue with new domains in the future.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;In short, there&#39;s a new phishing campaign targeting PyPI users occurring right now. The email asks you to &#34;verify their email address&#34; for &#34;account maintenance and security procedures&#34; with a note that your account may be suspended. This email is fake, and the link goes to &lt;code&gt;pypi-mirror.org&lt;/code&gt; which is a domain not owned by PyPI or the PSF.&lt;/p&gt; &lt;p&gt;If you have already clicked on the link and provided your credentials, we recommend changing your password on PyPI immediately. Inspect your account&#39;s Security History for anything unexpected. Report suspicious activity, such as potential phishing campaigns against PyPI, to &lt;a href=&#34;mailto:security@pypi.org&#34;&gt;&lt;code&gt;security@pypi.org&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&#34;what-is-pypi-doing-to-protect-users&#34;&gt;What is PyPI doing to protect users?&lt;/h2&gt; &lt;p&gt;There&#39;s no quick-and-easy method for PyPI maintainers to completely halt this sort of attack short of requiring phishing-resistant 2FA (such as hardware tokens). Below are the following steps we&#39;re taking to keep users safe:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Contacting the registrars and CDN of the malicious domains to have them taken down.&lt;/li&gt; &lt;li&gt;Submitting phishing domains to lists of known-malicious URLs. This makes browsers show a warning before visiting the website, hopefully triggering alarm bells for users.&lt;/li&gt; &lt;li&gt;Collaborating with other open source package managers to share strategies for quicker domain take-downs.&lt;/li&gt; &lt;li&gt;Exploring methods to make authenticating using TOTP-based 2FA more resistant to phishing.&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&#34;what-can-you-do-as-a-maintainer&#34;&gt;What can you do as a maintainer?&lt;/h2&gt; &lt;p&gt;If you are a maintainer of a package on PyPI, you can help protect your users by adopting the following practices:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Don&#39;t trust or click on links in emails that you didn&#39;t trigger yourself.&lt;/li&gt; &lt;li&gt;Use a password manager that auto-fills based on domain name and exclusively using this feature. If auto-fill isn&#39;t working when it usually does, that is a warning sign!&lt;/li&gt; &lt;li&gt;Adopt a phishing-resistant 2FA method such as hardware keys.&lt;/li&gt; &lt;li&gt;When in doubt, ask for help before taking action. There is no shame in being cautious, share fishy-looking emails with others.&lt;/li&gt; &lt;li&gt;Share this warning within your own communities. PyPI is not the first or last open source service that will be targeted with phishing attacks.&lt;/li&gt; &lt;/ul&gt;</description> <link>https://blog.pypi.org/posts/2025-09-23-plenty-of-phish-in-the-sea/</link> <pubDate>Tue, 23 Sep 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-09-23-plenty-of-phish-in-the-sea/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-09-23-plenty-of-phish-in-the-sea.png" type="image/png" length="70081" /> </item> <item> <title>Token Exfiltration Campaign via GitHub Actions Workflows</title> <author>Mike Fiedler</author> <category>security</category> <category>transparency</category> <description>&lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt; &lt;p&gt;I recently responded to an attack campaign where malicious actors injected code into GitHub Actions workflows attempting to steal PyPI publishing tokens. PyPI was not compromised, and no PyPI packages were published by the attackers.&lt;/p&gt; &lt;p&gt;Attackers targeted a wide variety of repositories, many of which had PyPI tokens stored as GitHub secrets, modifying their workflows to send those tokens to external servers. While the attackers successfully exfiltrated some tokens, they do not appear to have used them on PyPI.&lt;/p&gt; &lt;p&gt;I&#39;ve invalidated all affected tokens and notified the impacted project maintainers. If you&#39;re one of them, I have emailed you from &lt;a href=&#34;&amp;#109;&amp;#97;&amp;#105;&amp;#108;&amp;#116;&amp;#111;&amp;#58;&amp;#115;&amp;#101;&amp;#99;&amp;#117;&amp;#114;&amp;#105;&amp;#116;&amp;#121;&amp;#64;&amp;#112;&amp;#121;&amp;#112;&amp;#105;&amp;#46;&amp;#111;&amp;#114;&amp;#103;&#34;&gt;&amp;#115;&amp;#101;&amp;#99;&amp;#117;&amp;#114;&amp;#105;&amp;#116;&amp;#121;&amp;#64;&amp;#112;&amp;#121;&amp;#112;&amp;#105;&amp;#46;&amp;#111;&amp;#114;&amp;#103;&lt;/a&gt;.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;You can read more about the details of the attack on &lt;a href=&#34;https://blog.gitguardian.com/ghostaction-campaign-3-325-secrets-stolen/&#34;&gt;GitGuardian&#39;s blog&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&#34;timeline-and-response&#34;&gt;Timeline and Response&lt;/h2&gt; &lt;p&gt;On September 5th, a GitGuardian employee used the &lt;a href=&#34;../2024-03-06-malware-reporting-evolved/&#34;&gt;PyPI &#34;Report as malware&#34; button&lt;/a&gt; to submit their findings for a project named &lt;code&gt;fastuuid&lt;/code&gt; - namely they found a malicious GitHub Actions workflow attempting to exfiltrate PyPI tokens to a remote server. No compromise on PyPI was found, tokens relating to the user accounts were invalidated, and I reached out to the project owners to notify and help secure the account and project.&lt;/p&gt; &lt;p&gt;Later on September 5th, another researcher from GitGuardian emailed PyPI Security directly about their current findings, effectively an expansion of the previous attack. Due to some of the contents in that email, it ended up in our inbound Spam folder, delaying response until September 10th when I became aware of the attack via other channels, and found the original email in the Spam folder. I may follow up with our support system provider to see about improving spam filtering rules.&lt;/p&gt; &lt;p&gt;After triaging the situation, I discovered another Indicator of Compromise (IoC) in the form of a URL, which I shared with GitGuardian to assist with their ongoing investigation.&lt;/p&gt; &lt;p&gt;Over the course of the following few days, I reviewed the findings from the researchers. I observed that many of the project maintainers responded to notifications from the researchers on their open source issue trackers, either reverting the changes to their actions workflows, or removing the affected workflows entirely from history via force-push of the repository. Many of the maintainers also proactively rotated their PyPI tokens.&lt;/p&gt; &lt;p&gt;After confirming that no PyPI accounts had been compromised, on September 15th I reached out to the maintainers of the affected projects to notify them of the situation, to let them know that their tokens had been invalidated, and recommend using &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/&#34;&gt;Trusted Publishers&lt;/a&gt; with GitHub Actions to help protect their projects in the future.&lt;/p&gt; &lt;h2 id=&#34;what-you-can-do&#34;&gt;What You Can Do&lt;/h2&gt; &lt;p&gt;If you use GitHub Actions to publish to PyPI, I recommend the following steps to protect your projects:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Replace long-lived tokens with &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/&#34;&gt;Trusted Publishers&lt;/a&gt;. This is the most effective way to protect your projects from this type of attack. GitHub Trusted Publishers use short-lived tokens that are scoped to a specific repository, and expire after a short period of time.&lt;/li&gt; &lt;li&gt;Log into your account and review your security history for any suspicious activity. You can do this by going to your &lt;a href=&#34;https://pypi.org/manage/account/&#34;&gt;Account Settings&lt;/a&gt; and scrolling to the &lt;a href=&#34;https://pypi.org/manage/account/#account-events&#34;&gt;&#34;Security History&#34; section&lt;/a&gt;.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;While Trusted Publisher tokens can still be exfiltrated, using Trusted Publishers significantly reduces the risk of compromise.&lt;/p&gt; &lt;h2 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h2&gt; &lt;p&gt;Thanks to Charles Brossollet, Guillaume Valadon, and Gaëtan Ferry of &lt;a href=&#34;https://www.gitguardian.com/&#34;&gt;GitGuardian&lt;/a&gt; for their collaboration on this issue.&lt;/p&gt; &lt;p&gt;This investigation and incident response is made possible by the generosity of individuals and corporations supporting critical security efforts to better secure the Python community, with thanks to &lt;a href=&#34;https://alpha-omega.dev/&#34;&gt;Alpha-Omega&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Support these efforts and more here: &lt;a href=&#34;https://www.python.org/sponsors/application/&#34;&gt;https://www.python.org/sponsors/application/&lt;/a&gt;&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2025-09-16-github-actions-token-exfiltration/</link> <pubDate>Tue, 16 Sep 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-09-16-github-actions-token-exfiltration/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-09-16-github-actions-token-exfiltration.png" type="image/png" length="72989" /> </item> <item> <title>Preventing Domain Resurrection Attacks</title> <author>Mike Fiedler</author> <category>security</category> <description>&lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt; &lt;p&gt;PyPI now checks for expired domains to prevent domain resurrection attacks, a type of supply-chain attack where someone buys an expired domain and uses it to take over PyPI accounts through password resets.&lt;/p&gt; &lt;p&gt;These changes improve PyPI&#39;s overall account security posture, making it harder for attackers to exploit expired domain names to gain unauthorized access to accounts.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;Since early June 2025, PyPI has unverified over 1,800 email addresses when their associated domains entered expiration phases. This isn&#39;t a perfect solution, but it closes off a significant attack vector where the majority of interactions would appear completely legitimate.&lt;/p&gt; &lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt; &lt;p&gt;PyPI user accounts are linked to email addresses. Email addresses are tied to domain names; domain names can expire if unpaid, and someone else can purchase them.&lt;/p&gt; &lt;p&gt;During PyPI account registration, &lt;a href=&#34;https://policies.python.org/pypi.org/Terms-of-Service/#2-required-information&#34;&gt;users are required to verify their email addresses&lt;/a&gt; by clicking a link sent to the email address provided during registration. This verification ensures the address is valid and accessible to the user, and may be used to send important account-related information, such as password reset requests, or for PyPI Admins to use to contact the user.&lt;/p&gt; &lt;p&gt;PyPI considers the account holder&#39;s initially verified email address a strong indicator of account ownership. Coupled with a form of Two-Factor Authentication (2FA), this helps to further secure the account.&lt;/p&gt; &lt;p&gt;Once expired, an attacker could register the expired domain, set up an email server, issue a password reset request, and gain access to accounts associated with that domain name.&lt;/p&gt; &lt;p&gt;Accounts with any activity after &lt;a href=&#34;../2024-01-01-2fa-enforced/&#34;&gt;January 1 2024 will have 2FA enabled&lt;/a&gt;, and an attacker would need to have either the second factor, or perform a full account recovery.&lt;/p&gt; &lt;p&gt;For older accounts prior to the 2FA requirement date, having an email address domain expire could lead to account takeover, which is what we&#39;re attempting to prevent, as well as minimize potential exposure if an email domain &lt;em&gt;does&lt;/em&gt; expire and change hands, regardless of whether the account has 2FA enabled.&lt;/p&gt; &lt;p&gt;This is not an imaginary attack - this has happened at least once for &lt;a href=&#34;https://osv.dev/vulnerability/PYSEC-2022-199&#34;&gt;a PyPI project&lt;/a&gt; back in 2022, and &lt;a href=&#34;https://blog.illustria.io/illustria-discovers-account-takeover-vulnerability-in-a-popular-package-affecting-1000-8aaaf61ebfc4&#34;&gt;other package ecosystems&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;TL;DR: If a domain expires, don&#39;t consider email addresses associated with it verified any more.&lt;/strong&gt;&lt;/p&gt; &lt;h2 id=&#34;domain-expiration-timeframe&#34;&gt;Domain Expiration Timeframe&lt;/h2&gt; &lt;p&gt;Here&#39;s a generalized flowchart of phases that domain name registrars often adhere to. There&#39;s typically a grace period before a domain is deleted. Read more about &lt;a href=&#34;https://www.icann.org/resources/pages/registrant-about-errp-2018-12-07-en&#34;&gt;ICANN&#39;s Expired Registration Recovery Policy (ERRP)&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;One way to visualize this can be seen in the below flowchart:&lt;/p&gt; &lt;pre class=&#34;mermaid&#34;&gt;&lt;code&gt;flowchart TD A[Domain Active] --&amp;gt;|Expiration Date Reached| B{Owner Renews?} B --&amp;gt;|Yes - Within Grace Period| A B --&amp;gt;|No| C[Renewal Grace Period&amp;lt;br/&amp;gt;0-45 days] C --&amp;gt;|Owner Renews&amp;lt;br/&amp;gt;Regular Price| A C --&amp;gt;|No Renewal| D[Redemption Period&amp;lt;br/&amp;gt;30 days] D --&amp;gt;|Owner Redeems&amp;lt;br/&amp;gt;High Fee $70-200+| A D --&amp;gt;|No Redemption| E[Pending Delete&amp;lt;br/&amp;gt;5 days] E --&amp;gt;|Automatic| F[Domain Released]&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;The word &#34;expiration&#34; might be a bit overloaded, as conceptually there is no specific state advertised that a domain name has expired, rather using other indicators we can infer what state the domain name is currently in.&lt;/p&gt; &lt;p&gt;Thanks to our friends at &lt;a href=&#34;https://domainr.com/&#34;&gt;Domainr&lt;/a&gt; (a Fastly service), we can use their &lt;a href=&#34;https://domainr.com/docs/api/v2/status#status-results&#34;&gt;Status API&lt;/a&gt; to issue periodic queries for any given domain, and act on the response.&lt;/p&gt; &lt;p&gt;The time interval we&#39;ve chosen for now is 30 days, as per the flow above, there&#39;s a high likelihood that a domain is still in the Renewal Grace Period or Redemption Period when we check for status, and can react before it is released or changes hands.&lt;/p&gt; &lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: PyPI will not detect a non-expiring domain transfer, as we assume the parties are acting together to transfer a domain legitimately.&lt;/p&gt; &lt;h2 id=&#34;pypi-actions&#34;&gt;PyPI Actions&lt;/h2&gt; &lt;p&gt;After an initial bulk check period that took place in April 2025, PyPI will check daily for any domains in use for status changes, and update its internal database with the most recent status.&lt;/p&gt; &lt;p&gt;If a domain registration enters the redemption period, that&#39;s an indicator to PyPI that the previously verified email destinations may not be trusted, and will un-verify a previously-verified email address. PyPI will not issue a password reset request to addresses that have become unverified.&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Expired by date&#34; src=&#34;../../assets/2025-08-13-unverified-by-day.png&#34; /&gt;&lt;/p&gt; &lt;p&gt;Since the initial implementation early June 2025, PyPI has unverified over 1,800 email addresses (initial 1,500 excluded from chart), and will continue to do so daily, to protect both PyPI account holders, as well as the end users of PyPI packages.&lt;/p&gt; &lt;h2 id=&#34;recommendations-for-end-users&#34;&gt;Recommendations for end users&lt;/h2&gt; &lt;p&gt;If your PyPI account only has a &lt;strong&gt;single verified&lt;/strong&gt; email address from a custom domain name, add a &lt;strong&gt;second verified&lt;/strong&gt; email address from another notable domain (e.g. Gmail) to your account.&lt;/p&gt; &lt;p&gt;During a PyPI account recovery, PyPI may ask for other proofs, often via other services under the user&#39;s control. If the same email address is used on those other services, the recovery could appear legitimate. Ensure you have 2FA set on those services as well to prevent potential account takeovers.&lt;/p&gt; &lt;h2 id=&#34;thats-all-for-now-folks&#34;&gt;That&#39;s all for now, folks&lt;/h2&gt; &lt;p&gt;While these changes are not foolproof, they decrease the likelihood of domain resurrections account takeovers.&lt;/p&gt; &lt;p&gt;Thanks to Eric Case at &lt;a href=&#34;https://www.fastly.com/&#34;&gt;Fastly&lt;/a&gt; for helping us understand some of the complexities and Samuel Giddins at &lt;a href=&#34;https://rubycentral.org/&#34;&gt;Ruby Central&lt;/a&gt; for their initial ideas of this approach, and the &lt;a href=&#34;https://repos.openssf.org/&#34;&gt;OpenSSF Securing Software Repositories Working Group&lt;/a&gt; for their collaborative guidance on repository security.&lt;/p&gt; &lt;p&gt;This effort would not be possible without the continued support from &lt;a href=&#34;https://alpha-omega.dev/&#34;&gt;Alpha-Omega&lt;/a&gt;.&lt;/p&gt; &lt;h3 id=&#34;related-reading&#34;&gt;Related reading&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Domain_hijacking&#34;&gt;Wikipedia: Domain hijacking&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://capec.mitre.org/data/definitions/50.html&#34;&gt;CAPEC-50: Password Recovery Exploitation&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://capec.mitre.org/data/definitions/695.html&#34;&gt;CAPEC-695: Repo Jacking&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://repos.openssf.org/principles-for-package-repository-security&#34;&gt;OpenSSF: Principles for Package Repository Security&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://arxiv.org/abs/2112.10165&#34;&gt;arxiv: What are Weak Links in the npm Supply Chain?&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt;</description> <link>https://blog.pypi.org/posts/2025-08-18-preventing-domain-resurrections/</link> <pubDate>Mon, 18 Aug 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-08-18-preventing-domain-resurrections/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-08-18-preventing-domain-resurrections.png" type="image/png" length="72364" /> </item> <item> <title>PyPI now serves project status markers in API responses</title> <author>William Woodruff</author> <category>integrations</category> <category>security</category> <description>&lt;p&gt;PyPI now serves &lt;a href=&#34;https://packaging.python.org/en/latest/specifications/project-status-markers/&#34;&gt;project status markers&lt;/a&gt; in its standard &lt;a href=&#34;https://docs.pypi.org/api/index-api/&#34;&gt;index APIs&lt;/a&gt;. This allows downstream consumers (like Python package installers and index mirrors) to retrieve project statuses programmatically and use them to inform users when a project is archived or quarantined.&lt;/p&gt; &lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;PyPI has implemented project status markers as proposed and accepted in &lt;a href=&#34;https://peps.python.org/pep-0792/&#34;&gt;PEP 792&lt;/a&gt;.&lt;/li&gt; &lt;li&gt;As of today, PyPI supports three standard statuses: &lt;strong&gt;active&lt;/strong&gt; (the default), &lt;strong&gt;archived&lt;/strong&gt;, and &lt;strong&gt;quarantined&lt;/strong&gt;.&lt;/li&gt; &lt;li&gt;Downstream consumers can now retrieve these statuses via the standard index APIs and use them to inform users about the state of a project.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;See the &lt;a href=&#34;../2025-01-30-archival/&#34;&gt;project archival&lt;/a&gt; and &lt;a href=&#34;../2024-12-30-quarantine/&#34;&gt;project quarantine&lt;/a&gt; announcement posts for additional information on PyPI&#39;s implementation of those individual statuses.&lt;/p&gt; &lt;!-- more --&gt; &lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt; &lt;p&gt;Many Python regularly find themselves asking the same questions again and again when evaluating a new dependency:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Is the dependency deprecated, potentially in favor of another project?&lt;/li&gt; &lt;li&gt;If a vulnerability is discovered in the dependency, is it likely to be patched?&lt;/li&gt; &lt;li&gt;Can I expect major future changes to the dependency, or is it &#34;done&#34; (i.e. feature complete)?&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;These questions (and many others in the domain of supply chain security) essentially boil down to a single question: &lt;strong&gt;what is the status of this project?&lt;/strong&gt;&lt;/p&gt; &lt;h2 id=&#34;the-status-quo-before-status-markers&#34;&gt;The status quo before status markers&lt;/h2&gt; &lt;p&gt;Before &lt;a href=&#34;https://peps.python.org/pep-0792/&#34;&gt;PEP 792&lt;/a&gt;, Python packaging had no less than three overlapping solutions for determining a project&#39;s status:&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;Individual releases of a project could include a &lt;code&gt;Development Status&lt;/code&gt; &lt;a href=&#34;https://pypi.org/classifiers&#34;&gt;trove classifier&lt;/a&gt; in their metadata, such as &lt;code&gt;Development Status :: 7 - Inactive&lt;/code&gt; to indicate that the project is no longer actively maintained.&lt;/p&gt; &lt;p&gt;However, trove classifiers occur at the &lt;em&gt;distribution&lt;/em&gt; level, leading to two problems:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;To update a project&#39;s status, the project&#39;s maintainer must upload a new release with the updated classifier. This is unnecessarily onerous, particularly when the intent is to &lt;em&gt;stop&lt;/em&gt; updating the project!&lt;/li&gt; &lt;li&gt;Classifiers do not apply retroactively, meaning that all &lt;em&gt;previous&lt;/em&gt; releases of a project continue to have their original classifiers. This results in a misleading view of the project&#39;s status: a downstream that pins to &lt;code&gt;sampleproject==1.2.3&lt;/code&gt; may fail to realize that &lt;code&gt;sampleproject===1.2.4&lt;/code&gt; signals that the entire project is now inactive.&lt;/li&gt; &lt;/ol&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;Indices can mark individual files (or entire releases) as &#34;yanked,&#34; per the &lt;a href=&#34;https://packaging.python.org/en/latest/specifications/file-yanking/&#34;&gt;file yanking specification&lt;/a&gt;. Yanked files are effectively soft-deleted, meaning that they&#39;ll be skipped by complying installers during resolution but not if explicitly pinned by the user.&lt;/p&gt; &lt;p&gt;Yanking is a useful tool for mitigating accidental vulnerabilities or compatibility breakages in a release, but it has the same &#34;scope&#34; issue as classifiers: it applies at the file and release level, not at the project level.&lt;/p&gt; &lt;p&gt;Moreover, the semantics of yanking aren&#39;t appropriate for all potential statuses: soft deletion is still disruptive, whereas statuses like &#34;archived&#34; and &#34;deprecated&#34; suggest that the project is still suitable for installation, so long as the user can be made aware of its status.&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;PyPI itself has &#34;project statuses,&#34; which apply to the entire project. These statuses were not standardized, and therefore only appeared on user-facing HTML pages, not in the standard APIs. This made them difficult to retrieve programmatically, limiting their usefulness.&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Beyond these partial solutions many downstreams also apply heuristics to determine a project&#39;s status, such as checking for recent project (or source repository) activity or using popularity metrics like GitHub stars as a proxy for project health. However, these heuristics can be manipulated or outright incorrect, such as when a project is feature complete and therefore has no recent activity.&lt;/p&gt; &lt;p&gt;Overall, these partial solutions and heuristics point to a need for something better.&lt;/p&gt; &lt;h2 id=&#34;project-status-markers&#34;&gt;Project status markers&lt;/h2&gt; &lt;p&gt;That brings us to the new feature: project status markers.&lt;/p&gt; &lt;p&gt;Project status markers are a Python packaging standard derived from PyPI&#39;s existing project statuses. The standard defines four project statuses, which have both index-side and installer-side semantics:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;active&lt;/strong&gt;: Indicates that the project is active. This is the default status, meaning that any project that does not explicitly declare a status is considered active. Active projects are not subject to any restrictions on upload or installation.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;archived&lt;/strong&gt;: Indicates that the project does not expect to be updated in the future. When a project is &lt;strong&gt;archived&lt;/strong&gt;, PyPI will not allow new uploads to the project, and installers are encouraged to inform users about the project&#39;s archival.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;quarantined&lt;/strong&gt;: Indicates that the project is considered generally unsafe for use, e.g. due to malware. When a project is &lt;strong&gt;quarantined&lt;/strong&gt;, PyPI will not offer it for installation, and installers are encouraged to produce a warning when users attempt to install it&lt;sup id=&#34;fnref:warning&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:warning&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;deprecated&lt;/strong&gt;: Indicates that the project is considered obsolete, and may have been superceded by another project. Unlike archived projects, deprecated projects can still be uploaded to, but installers are encouraged to inform users about the project&#39;s deprecation.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Of these statuses, PyPI currently supports &lt;strong&gt;active&lt;/strong&gt;, &lt;strong&gt;archived&lt;/strong&gt;, and &lt;strong&gt;quarantined&lt;/strong&gt;. PyPI doesn&#39;t support &lt;strong&gt;deprecated&lt;/strong&gt; yet, but we&#39;ll be looking at supporting it now that the MVP is complete.&lt;/p&gt; &lt;p&gt;Beyond the statuses themselves, the standard also defines an optional &#34;status reason&#34; that can be used to provide additional context about the status. PyPI doesn&#39;t currently expose status reasons, but may do so in the future.&lt;/p&gt; &lt;h2 id=&#34;consuming-status-markers&#34;&gt;Consuming status markers&lt;/h2&gt; &lt;p&gt;The standard is one thing, but let&#39;s see how to actually get status markers from PyPI&#39;s &lt;a href=&#34;https://docs.pypi.org/api/index-api/&#34;&gt;index APIs&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Status markers are available in both the HTML and JSON index APIs. For the HTML API the &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; fields are:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;pypi:project-status&lt;/code&gt; for the project status itself (or &lt;strong&gt;active&lt;/strong&gt; by default)&lt;/li&gt; &lt;li&gt;&lt;code&gt;pypi:project-status-reason&lt;/code&gt; for the project status reason (if present)&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;For example:&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;curl --silent \ -H &#34;Accept: application/vnd.pypi.simple.v1+html&#34; \ https://pypi.org/simple/pepy/ \ | htmlq --pretty &#39;head meta[name=&#34;pypi:project-status&#34;]&#39;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Yields:&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-html&#34;&gt;&amp;lt;meta name=&#34;pypi:project-status&#34; content=&#34;archived&#34;&amp;gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Within the JSON API, the project status is available via the top-level &lt;code&gt;project-status&lt;/code&gt; object, which contains &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;reason&lt;/code&gt; fields corresponding to the HTML API fields above.&lt;/p&gt; &lt;p&gt;For example:&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-sh&#34;&gt;curl --silent \ -H &#34;Accept: application/vnd.pypi.simple.v1+json&#34; \ https://pypi.org/simple/pepy/ \ | jq &#39;.&#34;project-status&#34;&#39;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Yields:&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-json&#34;&gt;{ &#34;status&#34;: &#34;archived&#34;, }&lt;/code&gt;&lt;/pre&gt; &lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt; &lt;p&gt;Starting today, Python package installers and other index consumers can retrieve status markers from PyPI&#39;s standard index APIs.&lt;/p&gt; &lt;p&gt;Our hope is that downstreams &lt;em&gt;will&lt;/em&gt; consume these markers, and use them as suggested by the standard. In particular we hope that installers like &lt;a href=&#34;https://pip.pypa.io/&#34;&gt;pip&lt;/a&gt; and &lt;a href=&#34;https://docs.astral.sh/uv/&#34;&gt;uv&lt;/a&gt; will signal relevant statuses to users, helping them form a better picture of the status of their dependencies as well as set policies controlling which statuses are acceptable for installation.&lt;/p&gt; &lt;h2 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h2&gt; &lt;p&gt;&lt;a href=&#34;https://peps.python.org/pep-0792/&#34;&gt;PEP 792&lt;/a&gt; was authored by &lt;a href=&#34;https://github.com/woodruffw&#34;&gt;William Woodruff&lt;/a&gt; (Astral) and &lt;a href=&#34;https://github.com/facutuesca&#34;&gt;Facundo Tuesca&lt;/a&gt; (Trail of Bits). We&#39;d like to thank &lt;a href=&#34;https://github.com/dstufft&#34;&gt;Donald Stufft&lt;/a&gt; for being the PEP&#39;s sponsor and PEP delegate. Additionally, we&#39;d like to thank &lt;a href=&#34;https://github.com/di&#34;&gt;Dustin Ingram&lt;/a&gt; and &lt;a href=&#34;https://github.com/miketheman&#34;&gt;Mike Fiedler&lt;/a&gt; for their review and feedback on the PEP and the associated changes to PyPI.&lt;/p&gt; &lt;p&gt;The funding for this feature’s development comes in part from &lt;a href=&#34;https://alpha-omega.dev/&#34;&gt;Alpha-Omega&lt;/a&gt;. Alpha-Omega’s mission is to protect society by catalyzing sustainable security improvements to the most critical open-source software projects and ecosystems.&lt;/p&gt; &lt;div class=&#34;footnote&#34;&gt; &lt;hr /&gt; &lt;ol&gt; &lt;li id=&#34;fn:warning&#34;&gt; &lt;p&gt;This warning is technically moot, as PyPI itself will not offer any files from quarantined projects for installation. However, the warning can still help users understand &lt;em&gt;why&lt;/em&gt; their installation has failed, and is therefore recommended by the standard.&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:warning&#34; title=&#34;Jump back to footnote 1 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;/div&gt;</description> <link>https://blog.pypi.org/posts/2025-08-14-project-status-markers/</link> <pubDate>Thu, 14 Aug 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-08-14-project-status-markers/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-08-14-project-status-markers.png" type="image/png" length="74340" /> </item> <item> <title>Preventing ZIP parser confusion attacks on Python package installers</title> <author>Seth Larson</author> <category>deprecation</category> <category>publishing</category> <category>security</category> <description>&lt;p&gt;The Python Package Index is introducing new restrictions to protect Python package installers and inspectors from confusion attacks arising from ZIP parser implementations. This has been done in response to the discovery that the popular installer uv has a different extraction behavior to many Python-based installers that use the ZIP parser implementation provided by the &lt;code&gt;zipfile&lt;/code&gt; standard library module.&lt;/p&gt; &lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;ZIP archives constructed to exploit ZIP confusion attacks are now rejected by PyPI.&lt;/li&gt; &lt;li&gt;There is no evidence that this vulnerability has been exploited using PyPI.&lt;/li&gt; &lt;li&gt;PyPI is deprecating wheel distributions with incorrect &lt;code&gt;RECORD&lt;/code&gt; files.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Please see &lt;a href=&#34;https://astral.sh/blog/uv-security-advisory-cve-2025-54368&#34;&gt;this blog post&lt;/a&gt; and &lt;a href=&#34;https://github.com/astral-sh/uv/security/advisories/GHSA-8qf3-x8v5-2pj8&#34;&gt;CVE-2025-54368&lt;/a&gt; for more information on uv&#39;s patch.&lt;/p&gt; &lt;!-- more --&gt; &lt;h2 id=&#34;wheels-are-zips-and-zips-are-complicated&#34;&gt;Wheels are ZIPs, and ZIPs are complicated&lt;/h2&gt; &lt;p&gt;Python package &#34;wheels&#34; (or &#34;binary distributions&#34;), like many other file formats, actually a ZIP in disguise. The &lt;a href=&#34;https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT&#34;&gt;ZIP archive standard&lt;/a&gt; was created in 1989, where large archives might need to be stored across multiple distinct storage units due to size constraints. This requirement influenced the design of the ZIP archive standard, such as being able to update or delete already-archived files by appending new records to the end of a ZIP instead of having to rewrite the entire ZIP from scratch which might potentially be on another disk.&lt;/p&gt; &lt;p&gt;These design considerations meant that the ZIP standard is complicated to implement, and in many ways is ambiguous in what the &#34;result&#34; of extracting a valid ZIP file should be.&lt;/p&gt; &lt;p&gt;The &lt;a href=&#34;https://packaging.python.org/en/latest/specifications/binary-distribution-format/#binary-distribution-format&#34;&gt;&#34;Binary Distribution Format&#34; specification&lt;/a&gt; defines how a wheel is &lt;a href=&#34;https://packaging.python.org/en/latest/specifications/binary-distribution-format/#installing-a-wheel-distribution-1-0-py32-none-any-whl&#34;&gt;meant to be installed&lt;/a&gt;. However, the specification leaves many of the details on how exactly to extract the archive and handle ZIP-specific features to implementations. The most detail provided is:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;Although a specialized installer is recommended, a wheel file may be installed by simply unpacking into site-packages with the standard ‘unzip’ tool while preserving enough information to spread its contents out onto their final paths at any later time.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;This means that ZIP ambiguities are unlikely to be caught by installers, as there are no restrictions for which ZIP features are allowed in a valid wheel archive.&lt;/p&gt; &lt;p&gt;There&#39;s also a Python packaging specific mechanism for which files are meant to be included in a wheel. The &lt;code&gt;RECORD&lt;/code&gt; file included inside wheel &lt;code&gt;.dist-info&lt;/code&gt; directories lists files by name and optionally a checksum (like SHA256). The &lt;a href=&#34;https://packaging.python.org/en/latest/specifications/binary-distribution-format/#the-dist-info-directory&#34;&gt;specification for the &lt;code&gt;.dist-info&lt;/code&gt; directory&lt;/a&gt; details how installers are supposed to check the contents of the ZIP archive against &lt;code&gt;RECORD&lt;/code&gt;:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;Apart from &lt;code&gt;RECORD&lt;/code&gt; and its signatures, installation will fail if any file in the archive is not both mentioned and correctly hashed in &lt;code&gt;RECORD&lt;/code&gt;.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;However, most Python installers today do not do this check and extract the contents of the ZIP archive similar to &lt;code&gt;unzip&lt;/code&gt; and then amend the installed &lt;code&gt;RECORD&lt;/code&gt; within the virtual environment so that uninstalling the package works as expected.&lt;/p&gt; &lt;p&gt;This means that there is no forcing function on Python projects and packaging tools to follow packaging standards or normalize their use of ZIP archive features. This leads to the ambiguous situation today where no one installer can start enforcing standards without accidentally &#34;breaking&#34; projects and archives that already exist on PyPI.&lt;/p&gt; &lt;p&gt;PyPI is adopting a few measures to prevent attackers from abusing the complexities of ZIP archives and installers not checking &lt;code&gt;RECORD&lt;/code&gt; files to smuggle files past manual review processes and automated detection tools.&lt;/p&gt; &lt;h2 id=&#34;what-is-pypi-doing-to-prevent-zip-confusion-attacks&#34;&gt;What is PyPI doing to prevent ZIP confusion attacks?&lt;/h2&gt; &lt;p&gt;The correct method to unpack a ZIP is to first check the Central Directory of files before extracting entries. See this &lt;a href=&#34;https://www.crowdstrike.com/en-us/blog/how-to-prevent-zip-file-exploitation/&#34;&gt;blog post&lt;/a&gt; for a more detailed explanation of ZIP confusion attacks.&lt;/p&gt; &lt;p&gt;PyPI is implementing the following logic to prevent ZIP confusion attacks on the upload of wheels and ZIPs:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Rejecting ZIP archives with invalid record and framing information.&lt;/li&gt; &lt;li&gt;Rejecting ZIP archives with duplicate filenames in Local File and Central Directory headers.&lt;/li&gt; &lt;li&gt;Rejecting ZIP archives where files included in Local File and Central Directory headers don&#39;t match.&lt;/li&gt; &lt;li&gt;Rejecting ZIP archives with trailing data or multiple End of Central Directory headers.&lt;/li&gt; &lt;li&gt;Rejecting ZIP archives with incorrect End of Central Directory Locator values.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;PyPI already implements ZIP and tarball compression-bomb detection as a part of upload processing.&lt;/p&gt; &lt;p&gt;PyPI will also begin sending emails to &lt;strong&gt;warn users when wheels are published whose ZIP contents don&#39;t match the included &lt;code&gt;RECORD&lt;/code&gt; metadata file&lt;/strong&gt;. After 6 months of warnings, on February 1st, 2026, PyPI will begin &lt;strong&gt;rejecting&lt;/strong&gt; newly uploaded wheels whose ZIP contents don&#39;t match the included &lt;code&gt;RECORD&lt;/code&gt; metadata file.&lt;/p&gt; &lt;p&gt;We encourage all Python installers to use this opportunity to implement cross-checking of extracted wheel contents with the &lt;code&gt;RECORD&lt;/code&gt; metadata file.&lt;/p&gt; &lt;h2 id=&#34;record-and-zip-issues-in-top-python-packages&#34;&gt;&lt;code&gt;RECORD&lt;/code&gt; and ZIP issues in top Python packages&lt;/h2&gt; &lt;p&gt;Almost all the top 15,000 Python packages by downloads (of which 13,468 publish wheels) have no issues with the ZIP format or the &lt;code&gt;RECORD&lt;/code&gt; metadata file. This makes us confident that we can deploy these changes without major disruption of existing Python project development.&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Status&lt;/th&gt; &lt;th&gt;Number of Projects&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;No &lt;code&gt;RECORD&lt;/code&gt; or ZIP issues&lt;/td&gt; &lt;td&gt;13,460&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Missing file from &lt;code&gt;RECORD&lt;/code&gt;&lt;/td&gt; &lt;td&gt;4&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Mismatched &lt;code&gt;RECORD&lt;/code&gt; and ZIP headers&lt;/td&gt; &lt;td&gt;2&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Duplicate files in ZIP headers&lt;/td&gt; &lt;td&gt;2&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Other ZIP format issues&lt;/td&gt; &lt;td&gt;0&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Note that there are more occurrences of ZIP and &lt;code&gt;RECORD&lt;/code&gt; issues that have been reported for other projects on PyPI, but those projects are not in the top 15,000 by downloads.&lt;/p&gt; &lt;h2 id=&#34;what-actions-should-i-take&#34;&gt;What actions should I take?&lt;/h2&gt; &lt;p&gt;The mitigations above mean that users of PyPI, regardless of their installer, don&#39;t need to take immediate action to be safe. We recommend the following actions to users of PyPI to ensure compliance with Python package and ZIP standards:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;For users installing PyPI projects&lt;/strong&gt;: Make sure your installer tools are up-to-date.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;For maintainers of PyPI projects&lt;/strong&gt;: If you encounter an error during upload, read the error message and update your own build process or report the issue to your build tool, if applicable.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;For maintainers of installer projects&lt;/strong&gt;: Ensure that your ZIP implementation follows the ZIP standard and checks the Central Directory before proceeding with decompression. See the CPython &lt;code&gt;zipfile&lt;/code&gt; module for a ZIP implementation that implements this logic. Begin checking the &lt;code&gt;RECORD&lt;/code&gt; file against ZIP contents and erroring or warning the user that the wheel is incorrectly formatted.&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h2&gt; &lt;p&gt;Thanks to Caleb Brown (Google Open Source Security Team) and Tim Hatch (Netflix) for reporting this issue.&lt;/p&gt; &lt;p&gt;This level of coordination across Python ecosystem projects requires significant engineering time investment. Thanks to &lt;a href=&#34;https://alpha-omega.dev&#34;&gt;Alpha-Omega&lt;/a&gt; who sponsors the security-focused &lt;a href=&#34;https://www.python.org/psf/developersinresidence/&#34;&gt;Developer-in-Residence&lt;/a&gt; positions at the Python Software Foundation.&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2025-08-07-wheel-archive-confusion-attacks/</link> <pubDate>Thu, 07 Aug 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-08-07-wheel-archive-confusion-attacks/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-08-07-wheel-archive-confusion-attacks.png" type="image/png" length="72244" /> </item> <item> <title>PyPI Phishing Attack: Incident Report</title> <author>Mike Fiedler</author> <category>security</category> <category>transparency</category> <description>&lt;h1 id=&#34;incident-report-phishing-attack&#34;&gt;Incident Report: Phishing Attack&lt;/h1&gt; &lt;p&gt;Over the past few days, a phishing attack targeting PyPI users via email was uncovered. Our &lt;a href=&#34;../2025-07-28-pypi-phishing-attack/&#34;&gt;initial report&lt;/a&gt; was posted to raise awareness of the attack, and to provide some initial details on the attack vector.&lt;/p&gt; &lt;p&gt;Social media posts linking to the initial report have been shared widely, PyPI itself has not been breached with this attack.&lt;/p&gt; &lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;4 user accounts were successfully phished, now either disabled or credentials rotated&lt;/li&gt; &lt;li&gt;2 API Tokens were generated by the attackers, which have since been revoked&lt;/li&gt; &lt;li&gt;2 releases of the &lt;code&gt;num2words&lt;/code&gt; project were uploaded by the attacker, which have since been removed&lt;/li&gt; &lt;li&gt;The phishing domain has been taken down&lt;/li&gt; &lt;/ul&gt; &lt;!-- more --&gt; &lt;p&gt;This appears to be similar in nature to a recent incident involving &lt;code&gt;npm&lt;/code&gt; packages, where attackers successfully phished end-user credentials, and then compromised popular packages to distribute malware via npmjs.com. Read a &lt;a href=&#34;https://www.bleepingcomputer.com/news/security/popular-npm-linter-packages-hijacked-via-phishing-to-drop-malware/&#34;&gt;report on this attack here&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;To briefly recap the attack pattern, the attackers established a domain name, SSL certificate, and server running in a Virtual Private Server (VPS) in the cloud, which would transparently proxy requests to PyPI.org. This could be termed a &#34;forward proxy&#34;, many CDN providers operate their services this way. We cannot know for certain, but it appears that the attackers sent emails to addresses found in package metadata, which is publicly available due to users putting them in their &lt;code&gt;setup.py&lt;/code&gt; or &lt;code&gt;pyproject.toml&lt;/code&gt; files. The emails contained links to the phishing domain, asking them to verify their email address. When users clicked the link, they were directed to the phishing domain, which presented the PyPI.org website, but with a different URL in the browser address bar.&lt;/p&gt; &lt;p&gt;Normal traffic flow:&lt;/p&gt; &lt;pre class=&#34;mermaid&#34;&gt;&lt;code&gt;flowchart LR A[Client Browser] --&amp;gt;|Request| B[PyPI.org] B --&amp;gt;|Response| A&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Phishing attack flow:&lt;/p&gt; &lt;pre class=&#34;mermaid&#34;&gt;&lt;code&gt;flowchart LR A[Client Browser] --&amp;gt;|Click Email Link| B[Phishing Domain] B --&amp;gt;|Proxy Request| C[PyPI.org] C --&amp;gt;|Response| B B --&amp;gt;|Display Content| A&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;This may be termed &lt;a href=&#34;https://capec.mitre.org/data/definitions/94.html&#34;&gt;&#34;adversary in the middle&#34; (AiTM) attack&lt;/a&gt;, however the difference here is that the attacker is not intercepting traffic between the user and PyPI.org, rather is acting as a forward proxy, which is a common practice for content delivery networks (CDNs) and the like.&lt;/p&gt; &lt;p&gt;Since a browser&#39;s address bar showed &lt;code&gt;hxxps://pypj.org/...&lt;/code&gt;, and the website content of legitimate &lt;code&gt;pypi.org&lt;/code&gt;, users might have missed the small difference between a lowercase &lt;code&gt;i&lt;/code&gt; and &lt;code&gt;j&lt;/code&gt;, and tricked into thinking they were on the official PyPI website, and submitted their credentials to the phishing domain.&lt;/p&gt; &lt;p&gt;The phisher could then capture the credentials, and use them to log in to the real PyPI.org website, potentially compromising the user&#39;s account and any packages they maintain.&lt;/p&gt; &lt;p&gt;Having a form of two-factor authentication (2FA) enabled on the account generally prevents phishing attacks like this from being successful, as the attacker would need access to the second factor (e.g. a TOTP code) to complete the login process.&lt;/p&gt; &lt;p&gt;However, since the attacker was in between the user and PyPI.org, they could have captured the second factor as well, and could have used the TOTP code within a short time interval, or potentially even captured the session cookies provided during the response to the login request, and thus bypassed the need for 2FA for a short time.&lt;/p&gt; &lt;p&gt;If the user had enrolled a &lt;a href=&#34;https://pypi.org/help/#utfkey&#34;&gt;Security Device&lt;/a&gt; for PyPI second factor authentication, the attacker would not have been able to use the second factor, as the WebAuthn protocol requires the user to physically interact with a hardware security key, or use a browser-based implementation, which would not be possible if the user was not on the legitimate PyPI.org website (&lt;a href=&#34;https://www.w3.org/TR/webauthn-2/#relying-party&#34;&gt;Relying Party Identifier&lt;/a&gt;).&lt;/p&gt; &lt;p&gt;Also, there are a significant amount of relatively inactive users who pre-date 2FA on PyPI, and may not have 2FA enabled on their accounts. If a user in this category fell for the phishing attack, they would still need to complete email address verification, and 2FA enrollment. This process generates an email from PyPI to the user, which links back to the legitimate PyPI.org address. These user accounts were not as vulnerable to this specific attack.&lt;/p&gt; &lt;h2 id=&#34;timeline-of-events&#34;&gt;Timeline of events&lt;/h2&gt; &lt;p&gt;Times are in both Eastern Daylight Time (EDT) and UTC. Some events may be omitted for brevity.&lt;/p&gt; &lt;p&gt;&lt;em&gt;Keep in mind: in the USA, the weekend is Saturday and Sunday.&lt;/em&gt;&lt;/p&gt; &lt;h3 id=&#34;2025-07-26-saturday&#34;&gt;2025-07-26 Saturday&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;13:14 EDT (17:14 UTC): A user emails PyPI Security about a suspicious phishing email they received, which appears to be from PyPI.org, but with a different domain name.&lt;/li&gt; &lt;li&gt;14:01 EDT (18:01 UTC): A volunteer PyPI Admin posts a message about this phishing attack in the PyPI Admins chat.&lt;/li&gt; &lt;li&gt;18:37 EDT (22:37 UTC): A community member posts about their experience on &lt;a href=&#34;https://discuss.python.org/t/pypi-org-phishing-attack/100267&#34;&gt;Python Forums&lt;/a&gt;&lt;/li&gt; &lt;li&gt;19:18 EDT (23:18 UTC): On-call PyPI Admin sees the message and escalates to another volunteer PyPI Admin for assistance, while submitting abuse complaints to the domain registrar NameSilo (report #1) and content delivery network (CDN) provider Cloudflare (report #2).&lt;/li&gt; &lt;li&gt;19:38 EDT (23:38 UTC): A volunteer PyPI Admin responds, and begins investigation of the phishing attack.&lt;/li&gt; &lt;li&gt;20:07 EDT (00:07 UTC): The volunteer PyPI Admin shares findings and some actions taken in chat, and has to leave for personal reasons.&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&#34;2025-07-27-sunday&#34;&gt;2025-07-27 Sunday&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;06:34 EDT (10:34 UTC): On-call PyPI Admin updates that the registrar has indicated they should submit a different form, which they do. They also confirm that the &lt;a href=&#34;https://www.python.org/psf/trademarks/&#34;&gt;PSF Trademarks Working Group&lt;/a&gt; are also working on this from a trademark perspective to notify the CDN and registrar of the abuse.&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&#34;2025-07-28-monday&#34;&gt;2025-07-28 Monday&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;08:57 EDT (12:57 UTC): PyPI Admin staff sees the comments in chat, begins investigation follow up&lt;/li&gt; &lt;li&gt;09:20 EDT (13:20 UTC): Available PyPI Admins (volunteer and staff) &amp;amp; other PSF parties meet to determine next steps.&lt;/li&gt; &lt;li&gt;10:22 EDT (14:22 UTC): PyPI Admins post an &lt;a href=&#34;../2025-07-28-pypi-phishing-attack/&#34;&gt;initial report&lt;/a&gt; to the PyPI blog, and share it on social media.&lt;/li&gt; &lt;li&gt;10:44 EDT (14:44 UTC): PyPI Admins post a notice to &lt;a href=&#34;https://mail.python.org/mailman3/lists/pypi-announce.python.org/&#34;&gt;&lt;code&gt;pypi-announce&lt;/code&gt; mailing list&lt;/a&gt;, and a similar is posted to the more general &lt;a href=&#34;https://mail.python.org/mailman3/lists/security-announce.python.org/&#34;&gt;&lt;code&gt;security-announce&lt;/code&gt; mailing list&lt;/a&gt;.&lt;/li&gt; &lt;li&gt;11:37 EDT (15:37 UTC): A &lt;a href=&#34;https://github.com/pypi/warehouse/pull/18427&#34;&gt;new feature is added to PyPI&lt;/a&gt; to detect and warn users about phishing domains, which is deployed to production.&lt;/li&gt; &lt;li&gt;11:59 EDT (15:59 UTC): A threat hunter posts on Twitter about a finding in &lt;code&gt;num2words&lt;/code&gt; 0.5.15&lt;/li&gt; &lt;li&gt;12:01 EDT (16:01 UTC): Another PyPI Admin submits another abuse report to Cloudflare (report #3), with more context and details - requests, headers, IP address, and more.&lt;/li&gt; &lt;li&gt;12:05 EDT (16:05 UTC): Cloudflare response to initial abuse complaint from Saturday (report #2) as &#34;invalid&#34;.&lt;/li&gt; &lt;li&gt;12:18 EDT (16:18 UTC): PyPI Admins observe that Cloudflare placed a &#34;Suspected Phishing&#34; warning when visiting the phishing domain, reducing the probability for users to fall for the attack, despite declining the abuse reports.&lt;/li&gt; &lt;li&gt;12:20 EDT (16:20 UTC): NameSilo places the phishing domain under administrative &lt;code&gt;ClientHold&lt;/code&gt; status.&lt;/li&gt; &lt;li&gt;12:23 EDT (16:23 UTC): Cloudflare responds to second abuse report (report #3) as &#34;invalid&#34;.&lt;/li&gt; &lt;li&gt;12:38 EDT (16:38 UTC): &lt;code&gt;num2words&lt;/code&gt; project owner responds on Twitter confirming removal of 0.5.15&lt;/li&gt; &lt;li&gt;12:54 EDT (16:54 UTC): &lt;code&gt;num2words&lt;/code&gt; project owner responds on Twitter confirming removal of 0.5.16 as well as suspicious API Token&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;After the domain registrar NameSilo placed the domain name under &lt;code&gt;clientHold&lt;/code&gt; status, new DNS resolutions failed, as there are no name servers. Host records for a client to resolve the domain name to an IP address will then fail. As DNS caches expired, users no longer were able to resolve the domain name, and thus unable to access the phishing site.&lt;/p&gt; &lt;p&gt;Incident is considered resolved, PyPI Admins continue to monitor the situation, and analysis of the attack continues.&lt;/p&gt; &lt;h2 id=&#34;impact-analysis&#34;&gt;Impact Analysis&lt;/h2&gt; &lt;p&gt;The phishing attack was targeted at PyPI users, and since we were able to obtain at least one confirmed IP address of the hosting server, we have found 4 such successful phishing attacks, and have taken action on them.&lt;/p&gt; &lt;p&gt;A single account appears to have further activity beyond credentials being compromised, and the attacker uploaded releases to PyPI.org via a newly-provisioned API Token. &lt;code&gt;num2words&lt;/code&gt; versions 0.5.15 and 0.5.16 were uploaded to PyPI.org, which included malware, and removed within hours of being uploaded by the owner of the account.&lt;/p&gt; &lt;p&gt;The impacted user &lt;a href=&#34;https://nitter.tiekoetter.com/SFLinux/status/1949906299308953827&#34;&gt;posted about the activity on Twitter&lt;/a&gt;:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;Thanks for the heads up! There was a phishing attack on pypi this morning. The compromised version of num2words v 0.5.15 have been removed.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;and later:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;I found a weird token in our pypi account, probably the attacker had created it. I removed the token and deleted the malicious version again (0.5.16). I also created new backup TOTP codes for MFA. Will keep an eye on it but hopefully they won&#39;t be able to reupload the malware.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;An advisory has been published &lt;a href=&#34;https://osv.dev/vulnerability/PYSEC-2025-72&#34;&gt;PYSEC-2025-72&lt;/a&gt; to help end user and tooling to detect the malicious versions.&lt;/p&gt; &lt;h2 id=&#34;takeaways&#34;&gt;Takeaways&lt;/h2&gt; &lt;h3 id=&#34;anti-impersonation-email-prevention&#34;&gt;Anti-impersonation Email Prevention&lt;/h3&gt; &lt;p&gt;PyPI has settings for &lt;a href=&#34;https://postmarkapp.com/glossary/sender-policy-framework&#34;&gt;Sender Policy Framework (SPF)&lt;/a&gt;, &lt;a href=&#34;https://dkim.org/&#34;&gt;DomainKeys Identified Mail (DKIM)&lt;/a&gt;, and &lt;a href=&#34;https://dmarc.org/&#34;&gt;Domain-based Message Authentication, Reporting &amp;amp; Conformance (DMARC)&lt;/a&gt; to help prevent phishing attacks by verifying the authenticity of emails sent from PyPI.org.&lt;/p&gt; &lt;p&gt;However, each mail server that receives an email from PyPI.org is responsible for checking these values, and taking action based on the results of those checks. If the receiving mail server does not check these headers, or does not take action based on the results, then the email may still be delivered to the user&#39;s inbox. In this case, it appears that the phishing email was delivered to some users&#39; inboxes, despite the presence of these headers.&lt;/p&gt; &lt;p&gt;In other cases, the receiving mail server may have checked the headers, and presented the user with a &#34;Looks like Spam&#34; warning, leaving it up to the user to decide whether to trust the email or not. This is a common practice, as many email providers do not block emails outright, but instead mark them as spam or suspicious, allowing users to make the final decision.&lt;/p&gt; &lt;p&gt;Ultimately, it is up to the user to be vigilant and cautious when clicking links in emails, especially when the email is unexpected or from an unknown sender.&lt;/p&gt; &lt;h3 id=&#34;abuse-reporting-responses&#34;&gt;Abuse Reporting &amp;amp; Responses&lt;/h3&gt; &lt;p&gt;We were surprised that our reports to Cloudflare were rejected due to them being unable to confirm phishing, which lead to a longer-than-expected period of the phishing domain being online. We don&#39;t have additional visibility into why these reports were initially considered invalid by Cloudflare or why they did eventually result in a takedown, but we will review and determine if future reports could be made more actionable.&lt;/p&gt; &lt;h3 id=&#34;alternate-spelling-domain-names&#34;&gt;Alternate spelling domain names&lt;/h3&gt; &lt;p&gt;The PSF is exploring the transfer of domain names used in this attack and other similar domains to have them registered to the PSF, and prevent future abuse. This effort has significant costs associated with it, so the outcome is not guaranteed.&lt;/p&gt; &lt;p&gt;If successful, this would allow the PSF to take control of the domain names, and possibly redirect them to either a warning page, or to the official PyPI.org website, preventing future phishing attacks using these domain names.&lt;/p&gt; &lt;h2 id=&#34;call-to-action&#34;&gt;Call to action&lt;/h2&gt; &lt;p&gt;Some things you can do to help prevent this kind of attack in the future:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;If you have a dormant PyPI account, consider removing it if you do not need it. This will help reduce the number of potential targets for attackers.&lt;/li&gt; &lt;li&gt;If you have an older PyPI account, and have not logged in since 2FA has been required, consider logging in and enabling 2FA on your account. This will help protect your account from future phishing attacks.&lt;/li&gt; &lt;li&gt;Use WebAuthn via browser or hardware security keys for 2FA on your PyPI account. This will help protect your account from phishing attacks, as the attacker would need access to the second factor to complete the login process.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Security is a never-ending process, and we are always looking for ways to improve our security posture. If you have specific ideas or suggestions for improving PyPI security, and are willing to help implement them, consider checking our &lt;a href=&#34;https://github.com/pypi/warehouse/issues&#34;&gt;Issue Tracker&lt;/a&gt; or if you have something more sensitive, you can email &lt;a href=&#34;mailto:security@pypi.org&#34;&gt;security@pypi.org&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;This work would not be possible without generous donations, please consider &lt;a href=&#34;https://www.python.org/psf/&#34;&gt;supporting the PSF&lt;/a&gt; to ensure this kind of work can continue to serve the worldwide Python community.&lt;/p&gt; &lt;h2 id=&#34;indicators-of-compromise&#34;&gt;Indicators of Compromise&lt;/h2&gt; &lt;p&gt;Analysis exposed some indicators of compromise (IoCs) that may be useful for detecting this attack, and similar attacks in the future.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Domain name: &lt;code&gt;pypj.org&lt;/code&gt;&lt;/li&gt; &lt;li&gt;IP address: &lt;code&gt;45.9.148.108&lt;/code&gt; - the phishing email sending server&lt;/li&gt; &lt;li&gt;IP address: &lt;code&gt;45.9.148.85&lt;/code&gt; - the phishing domain hosting server&lt;/li&gt; &lt;li&gt;Domain name: &lt;code&gt;modirosa.com&lt;/code&gt; - used by attackers to establish accounts on PyPI.org&lt;/li&gt; &lt;li&gt;Domain name: &lt;code&gt;necub.com&lt;/code&gt; - used by attackers to establish accounts on PyPI.org&lt;/li&gt; &lt;li&gt;PyPI Package: &lt;code&gt;num2words&lt;/code&gt; - versions 0.5.15 and 0.5.16&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&#34;related-capecs&#34;&gt;Related CAPECs&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;&lt;a href=&#34;https://capec.mitre.org/data/definitions/94.html&#34;&gt;CAPEC-94&lt;/a&gt; - Adversary in the Middle (AiTM) Attack&lt;/li&gt; &lt;li&gt;&lt;a href=&#34;https://capec.mitre.org/data/definitions/98.html&#34;&gt;CAPEC-98&lt;/a&gt; - Phishing&lt;/li&gt; &lt;/ul&gt;</description> <link>https://blog.pypi.org/posts/2025-07-31-incident-report-phishing-attack/</link> <pubDate>Thu, 31 Jul 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-07-31-incident-report-phishing-attack/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-07-31-incident-report-phishing-attack.png" type="image/png" length="59242" /> </item> <item> <title>PyPI Users Email Phishing Attack</title> <author>Mike Fiedler</author> <category>security</category> <category>transparency</category> <description>&lt;p&gt;Read the follow-up post: &lt;a href=&#34;../2025-07-31-incident-report-phishing-attack/&#34;&gt;Phishing Attack Follow-Up&lt;/a&gt;&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;del&gt;(Ongoing, preliminary report)&lt;/del&gt;&lt;/p&gt; &lt;p&gt;PyPI has not been hacked, but users are being targeted by a phishing attack that attempts to trick them into logging in to a fake PyPI site.&lt;/p&gt; &lt;p&gt;Over the past few days, users who have published projects on PyPI with their email in package metadata may have received an email titled:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;[PyPI] Email verification&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;from the email address &lt;code&gt;noreply@pypj.org&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;Note the lowercase &lt;code&gt;j&lt;/code&gt; in the domain name, which is not the official PyPI domain, &lt;code&gt;pypi.org&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;This is not a security breach of PyPI itself, but rather a phishing attempt that exploits the trust users have in PyPI.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;The email instructs users to follow a link to verify their email address, which leads to a phishing site that looks like PyPI but is not the official site.&lt;/p&gt; &lt;p&gt;The user is prompted to log in, and the requests are passed back to PyPI, which may lead to the user believing they have logged in to PyPI, but in reality, they have provided their credentials to the phishing site.&lt;/p&gt; &lt;p&gt;PyPI Admins are looking into a few methods of handling this attack, and want to make sure users are aware of the phishing attempt while we investigate different options.&lt;/p&gt; &lt;p&gt;There is currently a banner on the PyPI homepage to warn users about this phishing attempt.&lt;/p&gt; &lt;p&gt;Always inspect the URL in the browser before logging in.&lt;/p&gt; &lt;p&gt;We are also waiting for CDN providers and name registrars to respond to the trademark and abuse notifications we have sent them regarding the phishing site.&lt;/p&gt; &lt;p&gt;If you have received this email, do not click on any links or provide any information. Instead, delete the email immediately.&lt;/p&gt; &lt;p&gt;If you have already clicked on the link and provided your credentials, we recommend changing your password on PyPI immediately. Inspect your account&#39;s Security History for anything unexpected.&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2025-07-28-pypi-phishing-attack/</link> <pubDate>Mon, 28 Jul 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-07-28-pypi-phishing-attack/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-07-28-pypi-phishing-attack.png" type="image/png" length="56891" /> </item> <item> <title>inbox.ru Domain Prohibition Follow-up</title> <author>Mike Fiedler</author> <category>security</category> <category>transparency</category> <description>&lt;p&gt;A follow-up to the &lt;a href=&#34;../2025-06-15-prohibiting-inbox-ru-emails/&#34;&gt;previous post&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;We have since learned that the campaign was orchestrated by the company that owns the &lt;code&gt;inbox.ru&lt;/code&gt; email domain, and not by a malicious third party as we initially suspected.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;Following the previous post, a representative of the parent company for &lt;code&gt;inbox.ru&lt;/code&gt; reached out to PyPI Admins to discuss the situation. They expressed their desire to resolve the issue, and reinstate the ability for their users to register for PyPI accounts with email addresses from the &lt;code&gt;inbox.ru&lt;/code&gt; domain.&lt;/p&gt; &lt;p&gt;They confirmed that the user account registrations on PyPI originated from an internal security team, &#34;to prevent possible abuse of external libraries for attacks on our systems&#34;.&lt;/p&gt; &lt;p&gt;They also confirmed that they have held staff meetings and have decided to abandon this practice, and develop alternate methods for detection and prevention of abuse, and have apologized for the incident.&lt;/p&gt; &lt;p&gt;As such, we have re-enabled the ability for users to register accounts using the &lt;code&gt;inbox.ru&lt;/code&gt; email domain, and to add &lt;code&gt;inbox.ru&lt;/code&gt; email addresses to existing accounts.&lt;/p&gt; &lt;p&gt;We will continue to monitor the situation, and if we see any further abuse from this domain or others, we will take appropriate action to protect PyPI users and resources.&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2025-07-25-inbox-ru-follow-up/</link> <pubDate>Fri, 25 Jul 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-07-25-inbox-ru-follow-up/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-07-25-inbox-ru-follow-up.png" type="image/png" length="55771" /> </item> <item> <title>Prohibiting inbox.ru email domain registrations</title> <author>Mike Fiedler</author> <category>security</category> <category>transparency</category> <description>&lt;p&gt;A recent spam campaign against PyPI has prompted an administrative action, preventing using the &lt;code&gt;inbox.ru&lt;/code&gt; email domain. This includes new registrations as well as adding as additional addresses.&lt;/p&gt; &lt;p&gt;The campaign created over 250 new user accounts, publishing over 1,500 new projects on PyPI, leading to end-user confusion, abuse of resources, and potential security issues.&lt;/p&gt; &lt;p&gt;All relevant projects have been removed from PyPI, and accounts have been disabled.&lt;/p&gt; &lt;!-- more --&gt; &lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt; &lt;p&gt;Users are welcome to use any valid email address to register a new account, however this delegates some of the responsibility of account security to the email provider.&lt;/p&gt; &lt;p&gt;PyPI uses the &lt;a href=&#34;https://github.com/disposable-email-domains/disposable-email-domains&#34;&gt;&lt;code&gt;disposable-email-domains&lt;/code&gt;&lt;/a&gt; list to prevent new registrations using disposable email addresses, and PyPI maintains its own internal block list, updated by PyPI Admins in response to discovering abuse.&lt;/p&gt; &lt;p&gt;See a &lt;a href=&#34;../2024-06-16-prohibiting-msn-emails/&#34;&gt;previous post&lt;/a&gt; for a previous case of prohibiting a popular email domain provider.&lt;/p&gt; &lt;h2 id=&#34;timeline&#34;&gt;Timeline&lt;/h2&gt; &lt;p&gt;Here&#39;s a timeline of the events I was able to put together&lt;/p&gt; &lt;ul&gt; &lt;li&gt;2025-06-09 first user account created, verified, 2FA set up, API Token provisioned&lt;/li&gt; &lt;li&gt;2025-06-11 46 more user accounts created over the course of 3 hours&lt;/li&gt; &lt;li&gt;2025-06-24 207 more user accounts created over the course of 4 hours&lt;/li&gt; &lt;li&gt;2025-06-29 New projects are created a file uploads start&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Here&#39;s a table showing how many projects were published on each date:&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Date&lt;/th&gt; &lt;th&gt;Number of Projects&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;2025-06-26&lt;/td&gt; &lt;td&gt;9&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;2025-06-27&lt;/td&gt; &lt;td&gt;295&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;2025-06-28&lt;/td&gt; &lt;td&gt;39&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;2025-06-29&lt;/td&gt; &lt;td&gt;119&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;2025-06-30&lt;/td&gt; &lt;td&gt;740&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;2025-07-01&lt;/td&gt; &lt;td&gt;249&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;2025-07-02&lt;/td&gt; &lt;td&gt;46&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;2025-07-10&lt;/td&gt; &lt;td&gt;16&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;2025-07-11&lt;/td&gt; &lt;td&gt;12&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Total: 1,525&lt;/p&gt; &lt;h2 id=&#34;details&#34;&gt;Details&lt;/h2&gt; &lt;p&gt;The projects were created with a variety of names with no code inside, so this wasn&#39;t a case of malware, but possibly using popular projects&#39; entrypoints (often a command line execution interface), as those are not required to be the same as the &lt;strong&gt;project name&lt;/strong&gt; on PyPI.&lt;/p&gt; &lt;p&gt;This pattern seems to be consistent with setting up a large number of accounts, confirming access, and then executing a large-scale &#34;attack&#34;. This may have proven to be a &#34;dry run&#34; for a future attack, depending on the success of this campaign.&lt;/p&gt; &lt;p&gt;PyPI Admins were alerted to this condition initially on 2025-07-08 from a user telling us that they were working with a Large Language Model (Sonnet 4) recommending installing a project that did not exist - aka &lt;a href=&#34;https://en.wikipedia.org/wiki/Slopsquatting&#34;&gt;&#34;slopsquatting&#34;&lt;/a&gt;. This is a good reminder that users should always verify the project names they are installing, and not copy &amp;amp; paste the name of a project suggested by a third party, such as an AI model or random person on the internet.&lt;/p&gt; &lt;p&gt;Thanks again to users who are vigilant and report issues to us!&lt;/p&gt; &lt;p&gt;Hopefully we can reverse this decision at some point in the future when we have more confidence in this email provider&#39;s ability to prevent abuse. If you work at this provider, please email us at &lt;a href=&#34;mailto:security@pypi.org&#34;&gt;security@pypi.org&lt;/a&gt; to discuss this decision.&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2025-06-15-prohibiting-inbox-ru-emails/</link> <pubDate>Tue, 15 Jul 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-06-15-prohibiting-inbox-ru-emails/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-06-15-prohibiting-inbox-ru-emails.png" type="image/png" length="66516" /> </item> <item> <title>Incident Report: Organizations Team privileges</title> <author>Ee Durbin</author> <category>security</category> <category>transparency</category> <description>&lt;p&gt;On April 14, 2025 &lt;a href=&#34;&amp;#109;&amp;#97;&amp;#105;&amp;#108;&amp;#116;&amp;#111;&amp;#58;&amp;#115;&amp;#101;&amp;#99;&amp;#117;&amp;#114;&amp;#105;&amp;#116;&amp;#121;&amp;#64;&amp;#112;&amp;#121;&amp;#112;&amp;#105;&amp;#46;&amp;#111;&amp;#114;&amp;#103;&#34;&gt;&amp;#115;&amp;#101;&amp;#99;&amp;#117;&amp;#114;&amp;#105;&amp;#116;&amp;#121;&amp;#64;&amp;#112;&amp;#121;&amp;#112;&amp;#105;&amp;#46;&amp;#111;&amp;#114;&amp;#103;&lt;/a&gt; was notified of a potential security concern relating to privileges granted to a PyPI User via Organization Teams membership persisting after the User was removed from the PyPI Organization the Team belongs to.&lt;/p&gt; &lt;p&gt;We validated the report as a true finding, identified all cases where this scenario had occurred, notified impacted parties, and released a fix. A full audit determined that all instances were accounted for, with no unauthorized actions taken as a result of the issue.&lt;/p&gt; &lt;!-- more --&gt; &lt;h2 id=&#34;timeline-of-events&#34;&gt;Timeline of events&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;2025-04-14 16:37 UTC A PyPI User who has been testing out our Organizations features noticed the issue and reported it according to our &lt;a href=&#34;https://pypi.org/security/&#34;&gt;Security Policy&lt;/a&gt; to &lt;a href=&#34;&amp;#109;&amp;#97;&amp;#105;&amp;#108;&amp;#116;&amp;#111;&amp;#58;&amp;#115;&amp;#101;&amp;#99;&amp;#117;&amp;#114;&amp;#105;&amp;#116;&amp;#121;&amp;#64;&amp;#112;&amp;#121;&amp;#112;&amp;#105;&amp;#46;&amp;#111;&amp;#114;&amp;#103;&#34;&gt;&amp;#115;&amp;#101;&amp;#99;&amp;#117;&amp;#114;&amp;#105;&amp;#116;&amp;#121;&amp;#64;&amp;#112;&amp;#121;&amp;#112;&amp;#105;&amp;#46;&amp;#111;&amp;#114;&amp;#103;&lt;/a&gt;.&lt;/li&gt; &lt;li&gt;2025-04-14 17:02 UTC PyPI Security acknowledges receipt.&lt;/li&gt; &lt;li&gt;2025-04-14 17:22 UTC PyPI Security validates the report as a true finding.&lt;/li&gt; &lt;li&gt;2025-04-14 17:58 UTC PyPI Security validating test and hot fix prepared for internal review.&lt;/li&gt; &lt;li&gt;2025-04-14 18:30 UTC PyPI Security removes invalid Team Membership and notifies the owners of the only other actively impacted Organization. &lt;a href=&#34;https://github.com/pypi/warehouse/pull/17957&#34;&gt;public PR&lt;/a&gt; opened with fix.&lt;/li&gt; &lt;li&gt;2025-04-14 18:33 UTC Hot fix is merged.&lt;/li&gt; &lt;li&gt;2025-04-14 18:39 UTC Hot fix deployed and live on PyPI.&lt;/li&gt; &lt;li&gt;2025-04-14 19:06 UTC Security audit complete, validating that only two instances of this had occurred, with no unauthorized actions taken as a result of the persisted privileges.&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&#34;details&#34;&gt;Details&lt;/h2&gt; &lt;p&gt;PyPI Organizations have been a feature on PyPI since they were first enabled on April 20, 2023. This issue was introduced in the initial development of Organizations features, and was mitigated April 14, 2025.&lt;/p&gt; &lt;p&gt;PyPI Organizations are quickly seeing more use as we (finally) exit our public beta period. In the last month we have gone from 70 Community Organization beta testers to 1,935 active Organizations&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;, so it is of little surprise that we are surfacing a few more issues as a result.&lt;/p&gt; &lt;p&gt;Thanks to PyPI&#39;s strong test coverage identifying and validating the issue was rather trivial, and getting a fix prepared and out the door was straight forward.&lt;/p&gt; &lt;p&gt;In total, this incident was resolved in 2 hours and 2 minutes from the time of report.&lt;/p&gt; &lt;h2 id=&#34;response&#34;&gt;Response&lt;/h2&gt; &lt;p&gt;Given that this is an otherwise straightforward bug, I thought I would take a moment to share how the issue was validated as well as how we audited. I&#39;ve replaced the specific organization, team, and user strings below, but otherwise all of this is copied and pasted from the terminal session used as I worked this report.&lt;/p&gt; &lt;p&gt;I spun up a local development environment of &lt;a href=&#34;https://github.com/pypi/warehouse&#34;&gt;pypi/warehouse&lt;/a&gt; from the current &lt;code&gt;main&lt;/code&gt; branch locally and followed the reporter&#39;s steps to reproduce:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;The basic reproduce steps were:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Add a user to an organization as a member&lt;/li&gt; &lt;li&gt;Add that member to a organization team&lt;/li&gt; &lt;li&gt;Remove the member from the organization&lt;/li&gt; &lt;/ol&gt; &lt;/blockquote&gt; &lt;p&gt;Noting that indeed, the User&#39;s team role persisted, and they could continue to act with those privileges on PyPI.&lt;/p&gt; &lt;p&gt;At that point the reporter and PyPI Administrators team were notified that we had a finding, and that review would be needed shortly to get a fix merged and deployed.&lt;/p&gt; &lt;p&gt;From there, I added a &lt;a href=&#34;https://github.com/pypi/warehouse/pull/17957/commits/33707f0ad72e4d2efacf85fd0488e0c42fca47e6&#34;&gt;failing test&lt;/a&gt; which further validated the issue, and got to work creating a &lt;a href=&#34;https://github.com/pypi/warehouse/pull/17957/commits/34a40178ee7d0e048e45867a9d8f76497f68da8c&#34;&gt;patch&lt;/a&gt; which turned the test green.&lt;/p&gt; &lt;p&gt;Now, with time to wait while a volunteer PyPI Admin returned I focused on assessing if this was actively impacting any other organizations:&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code&gt;warehouse=&amp;gt; select o.name as organization, t.name as team_name, u.username as user, tr. role_name as team_role, ors. role_name as organization role from team_roles tr join teams t on t.id=tr.team_id join organizations o on t.organization_id=o.id join users u on u.id=tr.user_id left outer join organization roles ors on ors.organization_id=t.organization_id and ors.user_id=tr.user_id where ors. role_name is null; organization | team_name | user | team_role | organization_role --------------+-------------+-----------+-----------+------------------- spam | Spam-owners | spamlover | Member | (1 row)&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;This query showed me that one instance of a User having an Organization Team Role &lt;em&gt;without&lt;/em&gt; being a Member of that Organization still existed on PyPI&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;. The reporter made clear that they had already resolved the instance from their testing.&lt;/p&gt; &lt;p&gt;I drafted a notice to the five users with role &lt;code&gt;Owner&lt;/code&gt; on the impacted Organization, and took a moment to realize that this was our first time emailing Organization Owners as a group, and that we needed to account for the fact that Users on PyPI do not necessarily already know one-another&#39;s email addresses, as it is not required to invite them to a Project or Organization. A quick gut-check in the PyPI Moderators channel validated my plan to &lt;code&gt;Bcc:&lt;/code&gt; all the Owners rather than &lt;code&gt;To:&lt;/code&gt; them as a group.&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt; &lt;p&gt;By that point, the volunteer PyPI Administrator was available to review the PR and drafted e-mail. We notified the impacted Organization, and then coordinated to open the PR publicly and approve/merge it hastily before completing a more in-depth audit.&lt;/p&gt; &lt;p&gt;Luckily this audit was straightforward using our internal security records combined with the fact that there has been minimal churn in the Organization membership in the short time that Organizations has been in broader use.&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code&gt;warehouse=&amp;gt; select o.name, time, tag, u.username from organization_events oe join users u on (additional-&amp;gt;&amp;gt;&#39;target_user_id&#39;)::uuid=u.id join organizations o on oe.source_id=o.id where tag in (&#39;organization:team_role:remove&#39;, &#39;organization:organization_role:remove&#39;) order by time; name | time | tag | username ------------+----------------------------+---------------------------------------+------------------- lumberjack | 2023-05-02 03:01:18.935901 | organization:organization_role:remove | sirrobin holygrail | 2023-07-06 12:55:43.261593 | organization:organization_role:remove | blackknight ni | 2023-09-18 12:07:17.389244 | organization:organization_role:remove | shrubbery parrot | 2024-02-04 19:23:25.354344 | organization:organization_role:remove | exparrot spam | 2024-08-24 01:40:22.405746 | organization:organization_role:remove | spamlover spam | 2025-02-09 18:14:13.891224 | organization:team_role:remove | eggandspam albatross | 2025-03-07 06:55:29.446617 | organization:organization_role:remove | nudge albatross | 2025-03-07 06:55:37.271176 | organization:organization_role:remove | wink cheese | 2025-03-13 18:25:54.650905 | organization:team_role:remove | gorgonzola cheese | 2025-03-13 18:26:02.525162 | organization:team_role:remove | camembert ministry | 2025-03-20 07:53:45.616404 | organization:organization_role:remove | sillywalks argument | 2025-03-31 15:52:18.186223 | organization:organization_role:remove | contradiction fishslap | 2025-04-14 15:12:14.023183 | organization:organization_role:remove | danceking fishslap | 2025-04-14 15:24:54.208641 | organization:organization_role:remove | danceking fishslap | 2025-04-14 15:27:22.954624 | organization:team_role:remove | danceking&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Here, we see the &lt;code&gt;spamlover&lt;/code&gt; user being removed from the &lt;code&gt;spam&lt;/code&gt; Organization on &lt;code&gt;2024-08-24&lt;/code&gt;, without being removed from the team, confirming our finding from the earlier query.&lt;/p&gt; &lt;p&gt;We also see the User &lt;code&gt;danceking&lt;/code&gt; from the &lt;code&gt;fishslap&lt;/code&gt; Organization being removed from the Organization multiple times, before the reporter removed them from their assigned Team.&lt;/p&gt; &lt;p&gt;This allowed us to confirm that beyond the already identified incidents, no other Organizations had found this problem before without letting us know.&lt;/p&gt; &lt;h2 id=&#34;thanks&#34;&gt;Thanks&lt;/h2&gt; &lt;p&gt;First and foremost, thanks to our reporters, Matthew Treinish and Jake Lishman of IBM Quantum for finding and reporting this issue.&lt;/p&gt; &lt;p&gt;We are grateful for the entire community of security researchers and users who find and report security issues to PyPI in accordance with our &lt;a href=&#34;https://pypi.org/security/&#34;&gt;Security Policy&lt;/a&gt;. PyPI relies on the efforts of our community to help us find and resolve issues like these before they become critical issues. Cooperation between all parties helps to improve the security of open source, and none of us could do it alone.&lt;/p&gt; &lt;p&gt;The tools and capabilities we&#39;ve evolved in PyPI over the past six years have really come to be an asset in situations like these. I&#39;m grateful to all the contributors and admins who have helped us to build them 💜.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;em&gt;Ee Durbin is the Director of Infrastructure at the Python Software Foundation. They have been contributing to keeping PyPI online, available, and secure since 2013.&lt;/em&gt;&lt;/p&gt; &lt;div class=&#34;footnote&#34;&gt; &lt;hr /&gt; &lt;ol&gt; &lt;li id=&#34;fn:1&#34;&gt; &lt;p&gt;As of writing, there are 6,682 remaining Organization Requests to review.&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; title=&#34;Jump back to footnote 1 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;li id=&#34;fn:2&#34;&gt; &lt;p&gt;It also showed me that our modeling could certainly be improved. In general all the joins are fine, but the fact that a &lt;code&gt;TeamRole&lt;/code&gt; is directly related to a &lt;code&gt;User&lt;/code&gt; rather than to their &lt;code&gt;OrganizationRole&lt;/code&gt; allowed for this disconnect in the first place.&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; title=&#34;Jump back to footnote 2 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;li id=&#34;fn:3&#34;&gt; &lt;p&gt;Another thing to work on moving forward. We recently added some &#34;in-app&#34; messaging for PyPI Admins and Support to contact users regarding Organization Requests, which could be useful for group communication with Organization Owners.&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; title=&#34;Jump back to footnote 3 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;/div&gt;</description> <link>https://blog.pypi.org/posts/2025-04-14-incident-report-organization-team-privileges/</link> <pubDate>Mon, 14 Apr 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-04-14-incident-report-organization-team-privileges/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-04-14-incident-report-organization-team-privileges.png" type="image/png" length="72872" /> </item> <item> <title>Introducing our new Terms of Service</title> <author>Ee Durbin</author> <category>policies</category> <category>transparency</category> <description>&lt;p&gt;We&#39;re introducing a new &lt;a href=&#34;https://policies.python.org/pypi.org/Terms-of-Service/&#34;&gt;Terms of Service&lt;/a&gt; to formalize our relationship to users and enable us to move forward with providing new features and services, specifically &lt;a href=&#34;https://docs.pypi.org/organization-accounts/&#34;&gt;Organization Accounts&lt;/a&gt;.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;PyPI has had some form of &lt;a href=&#34;https://policies.python.org/pypi.org/Terms-of-Use/&#34;&gt;Terms of Use&lt;/a&gt; document for users since it &lt;a href=&#34;https://github.com/pypi/legacy/commit/b139c00cfc5794159afb1fc185d77dbc5fc1a2a4#diff-a67499b048e6bb6ef08d44c7a3c541199615b68e3bd153eb0ccedc492e3dec9dR7-R13&#34;&gt;began accepting uploads in 2005&lt;/a&gt; and has only been updated twice&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; since. These terms have primarily served to protect PyPI and the Python Software Foundation (PSF) who operates it.&lt;/p&gt; &lt;p&gt;Over time we have introduced additional policies to protect our users and community such as our &lt;a href=&#34;https://policies.python.org/python.org/code-of-conduct/&#34;&gt;Code of Conduct&lt;/a&gt; &lt;a href=&#34;https://policies.python.org/pypi.org/Privacy-Notice/&#34;&gt;Privacy Notice&lt;/a&gt; and &lt;a href=&#34;https://policies.python.org/pypi.org/Acceptable-Use-Policy/&#34;&gt;Acceptable Use Policy&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Our new &lt;a href=&#34;https://policies.python.org/pypi.org/Terms-of-Service/&#34;&gt;Terms of Service&lt;/a&gt; formalizes our relationship to PyPI users, makes protections for the PSF and PyPI users more explicit, and establishes terms we need to provide &lt;a href=&#34;https://docs.pypi.org/organization-accounts/&#34;&gt;Organization Accounts&lt;/a&gt; to paid &lt;a href=&#34;https://docs.pypi.org/organization-accounts/pricing-and-payments/#corporate-organizations&#34;&gt;Corporate Organizations&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;We have worked with our legal team to retain compatibility with the superseded &lt;a href=&#34;https://policies.python.org/pypi.org/Terms-of-Use/&#34;&gt;Terms of Use&lt;/a&gt; while adding as permissive a set of new terms as possible to ensure that PyPI users and the PSF are protected.&lt;/p&gt; &lt;p&gt;You will notice a banner on login reminding you of these updated terms, as well as an email notification to your primary email address if it has been verified. These terms will take effect for existing users March 27, 2025 and your continued use of PyPI after that date constitutes agreement to these new terms.&lt;/p&gt; &lt;div class=&#34;footnote&#34;&gt; &lt;hr /&gt; &lt;ol&gt; &lt;li id=&#34;fn:1&#34;&gt; &lt;p&gt;See these commits for substantive changes since the Terms of Use was introduced: &lt;a href=&#34;https://github.com/pypi/legacy/commit/ddbd32a78a431ab46cad912046c2492998edc618#diff-a6e30135c956f467cffa36eb37a756a53921754d55ddd6ea80d2a0b4c3f4abfaR16-R33&#34;&gt;2009-11-29&lt;/a&gt; and &lt;a href=&#34;https://github.com/pypi/legacy/commit/f645942c65a372fdacd4d48ffb4afed4502632e8#diff-bbf95bcc6416475537256acea89690f7c6b1f965c0306e9b883813bd3e4f6c10R15-R98&#34;&gt;2016-12-16&lt;/a&gt;.&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; title=&#34;Jump back to footnote 1 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;/div&gt;</description> <link>https://blog.pypi.org/posts/2025-02-25-terms-of-service/</link> <pubDate>Tue, 25 Feb 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-02-25-terms-of-service/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-02-25-terms-of-service.png" type="image/png" length="59303" /> </item> <item> <title>PyPI Now Supports Project Archival</title> <author>Facundo Tuesca</author> <category>security</category> <description>&lt;p&gt;Support for marking projects as archived has landed on PyPI. Maintainers can now archive a project to let users know that the project is not expected to receive any more updates.&lt;/p&gt; &lt;p&gt;This allows users to make better decisions about which packages they depend on, especially regarding supply-chain security, since archived projects clearly signal that no future security fixes or maintenance should be expected.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;Project archival is not deletion: archiving a project does not remove it from the index, and does not prevent users from installing it. Archival is &lt;em&gt;purely&lt;/em&gt; a user-controlled marker that gives project owners the ability to signal a project’s status; PyPI has no plans to delete or prune archived distributions.&lt;/p&gt; &lt;p&gt;Support for archival is built on top of the project quarantine feature. Read more about that feature in &lt;a href=&#34;../2024-12-30-quarantine/&#34;&gt;PyPI’s December 2024 blog post&lt;/a&gt;. You can also find more details about the project archival’s implementation on the &lt;a href=&#34;https://blog.trailofbits.com/2025/01/30/pypi-now-supports-archiving-projects/&#34;&gt;Trail of Bits blog&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&#34;archiving-a-project&#34;&gt;Archiving a project&lt;/h2&gt; &lt;p&gt;Owners of a project can archive it by navigating to the project’s settings page and scrolling down near the end to the following section:&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Screenshot showing the archival section in the project settings page.&#34; src=&#34;../../assets/2025-01-30-archival/screenshot-settings.png&#34; /&gt;&lt;/p&gt; &lt;p&gt;As described in the warning message, archiving prevents new uploads to the project. After archiving the project, users will see the following notice in the project’s main PyPI page:&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Screenshot showing the message displayed in the project page when a project is archived.&#34; src=&#34;../../assets/2025-01-30-archival/screenshot-message.png&#34; /&gt;&lt;/p&gt; &lt;p&gt;Maintainers are encouraged to make a final release before archiving, updating the project’s description with more context about the archival.&lt;/p&gt; &lt;p&gt;Finally, the project owners can always unarchive a project if needed.&lt;/p&gt; &lt;h2 id=&#34;stay-tuned&#34;&gt;Stay tuned&lt;/h2&gt; &lt;p&gt;Project archival is the first step in a larger project, aimed at improving the &lt;em&gt;lifecycle&lt;/em&gt; of projects on PyPI. That project includes evaluating additional project statuses (things like “deprecated” and “unmaintained”), as well as changes to &lt;a href=&#34;https://docs.pypi.org/api/&#34;&gt;PyPI’s public APIs&lt;/a&gt; that will enable clients to retrieve and act on project status information. You can track our progress on these fronts by following along with &lt;a href=&#34;https://github.com/pypi/warehouse/issues/16844&#34;&gt;warehouse#16844&lt;/a&gt;!&lt;/p&gt; &lt;h2 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h2&gt; &lt;p&gt;This feature was developed by &lt;a href=&#34;https://www.trailofbits.com/&#34;&gt;Trail of Bits&lt;/a&gt;. We would like to thank the PyPI admins and maintainers, including &lt;a href=&#34;https://github.com/miketheman&#34;&gt;Mike Fiedler&lt;/a&gt; and &lt;a href=&#34;https://github.com/di&#34;&gt;Dustin Ingram&lt;/a&gt;, for their time and consideration throughout the design and development process.&lt;/p&gt; &lt;p&gt;The funding for this feature’s development comes from &lt;a href=&#34;https://alpha-omega.dev/&#34;&gt;Alpha-Omega&lt;/a&gt;. Alpha-Omega’s mission is to protect society by catalyzing sustainable security improvements to the most critical open-source software projects and ecosystems.&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2025-01-30-archival/</link> <pubDate>Thu, 30 Jan 2025 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2025-01-30-archival/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2025-01-30-archival.png" type="image/png" length="60057" /> </item> <item> <title>Project Quarantine</title> <author>Mike Fiedler</author> <category>security</category> <description>&lt;p&gt;Earlier this year, I wrote briefly about new functionality added to PyPI, the &lt;a href=&#34;../2024-08-16-safety-and-security-engineer-year-in-review/#project-lifecycle-status-quarantine&#34;&gt;ability to quarantine projects&lt;/a&gt;. This feature allows PyPI administrators to mark a project as potentially harmful, and prevent it from being easily installed by users to prevent further harm.&lt;/p&gt; &lt;p&gt;In this post I&#39;ll discuss the implementation, and further improvements to come.&lt;/p&gt; &lt;!-- more --&gt; &lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt; &lt;p&gt;Malware on PyPI is a persistent problem.&lt;/p&gt; &lt;p&gt;PyPI has concepts of Projects, Releases, and Files&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. These are all discrete data models&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;, and behave slightly differently based on their characteristics. A Project may have 0 or more Releases, a Release may have 1 or more Files.&lt;/p&gt; &lt;p&gt;Researchers will often report a given Project as malware, and will link to a specific location in a File for a given Release, per the &lt;a href=&#34;https://pypi.org/security/&#34;&gt;PyPI Security process&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;PyPI will receive malware reports&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34;&gt;3&lt;/a&gt;&lt;/sup&gt; that are often relevant to an entire Project. Simply put: a Project, along with all of its Releases (usually 1) and Files (usually 1-2) are all part of a similar campaign, and should be removed from PyPI to protect end users. This is not universally true, as malware has been added to established, mature Projects via a new Release after some sort of account access takeover, so there may be a need to consider reporting malware for a given Release/File - something not yet fully implemented via Observations or the &lt;a href=&#34;../2024-03-06-malware-reporting-evolved/#via-api&#34;&gt;beta Malware API&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;When reviewing and acting on malware reports, PyPI Admins had one main tool at their disposal: &lt;strong&gt;complete removal of the Project from the PyPI database&lt;/strong&gt;. This is often coupled with prohibiting the Project name from being reused. PyPI has functionality irrespective of malware to prevent File name reuse.&lt;/p&gt; &lt;p&gt;The impact of these removals can be disruptive, and removals are pretty much irrevocable - it&#39;s the same mechanism PyPI warns project owners about when they elect to remove their project from the index&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt; &lt;p&gt;Further, the longer a malicious Project remains publicly available, the greater the potential for end users to install and become victims of said malware. With the current full-time security staff for PyPI == 1, there is potential for malware to remain installable by users for longer periods of time, and asking volunteer PyPI Admins for extra hours of work is not sustainable.&lt;/p&gt; &lt;p&gt;Reducing the time window when a malicious Project/Release/File is available for end users to become victims is an improvement, and further reduces the incentive for malicious actors to use PyPI as their distribution method.&lt;/p&gt; &lt;h2 id=&#34;implementation&#34;&gt;Implementation&lt;/h2&gt; &lt;p&gt;The implementation of Project Quarantine shape as I learned more about the possible states a project could be in. I jotted down some basic requirements for the feature:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Project exists on PyPI that has Releases and Files&lt;/li&gt; &lt;li&gt;Project is not installable (hidden from simple index) while in quarantine&lt;/li&gt; &lt;li&gt;Project is not modifiable by the project owner while in quarantine&lt;/li&gt; &lt;li&gt;Project state is visible to Project Owners, security researchers, and PyPI Administrators&lt;/li&gt; &lt;li&gt;Project state can be reverted by a PyPI Administrator to restore general visibility&lt;/li&gt; &lt;li&gt;Project can be removed/deleted by a PyPI Administrator&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;With those in mind, I set out to implement the feature.&lt;/p&gt; &lt;h3 id=&#34;take-a-page-from-the-book-of-yank&#34;&gt;Take a page from the book of Yank&lt;/h3&gt; &lt;p&gt;Prior to this change, an existing feature was &#34;Yank&#34;, per &lt;a href=&#34;https://peps.python.org/pep-0592/&#34;&gt;PEP 592&lt;/a&gt;. &lt;/p&gt; &lt;p&gt;A Project with no Releases will be listed in the Simple Repository API&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;, but the resulting detail page will not have any links, making it effectively uninstallable&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:6&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;. One idea was when quarantining a Project, we could mark it as having no Releases, and thus excluding it from the index.&lt;/p&gt; &lt;p&gt;The difference from &#34;yank&#34; is that a yanked Release is still installable by clients, and quarantined items should not be installable - so we&#39;d have to explore where to make the change and how that would impact clients. Yank is also applied to a Release (and all of its Files), not a Project. We could apply a change to every Release for a Project, instead of Project-wide, and thus set ourselves up for quarantining individual Releases.&lt;/p&gt; &lt;p&gt;This ends up more complex, trying to account for a rare edge case where a mature Project has a new Release that needs to be quarantined, and would prevent disruption of existing users of prior Releases.&lt;/p&gt; &lt;p&gt;We accept that this might happen, and track very closely if and when it does, and defer implementation until that time.&lt;/p&gt; &lt;h3 id=&#34;create-an-observer-only-visibility-api&#34;&gt;Create an Observer-only visibility API&lt;/h3&gt; &lt;p&gt;I had previously built a new beta API infrastructure to allow Observers to report malicious Projects. One idea was to add a new authenticated API endpoint to allow querying the current list of quarantined Projects, and supply links to their Releases and Files for consumption.&lt;/p&gt; &lt;p&gt;Thus, a researcher could download the artifacts in question, but not via &lt;code&gt;pip install ...&lt;/code&gt;&lt;/p&gt; &lt;p&gt;I ended up not pursuing this approach, as the beta authenticated APIs are still being developed, and I didn&#39;t want to add more functionality before we swing back and figure out some critical authentication and authorization issues needed for the future of management API endpoints.&lt;/p&gt; &lt;h3 id=&#34;lifecycle-status&#34;&gt;Lifecycle Status&lt;/h3&gt; &lt;p&gt;The exploration to remove items from the Simple Repository API paid off, and pointed me in the direction that turned into &lt;code&gt;LifecycleStatus&lt;/code&gt;, which is a new status applied to a Project.&lt;/p&gt; &lt;p&gt;A state diagram to illustrate the flow of the Project through the states:&lt;/p&gt; &lt;pre class=&#34;mermaid&#34;&gt;&lt;code&gt;stateDiagram-v2 [*] --&amp;gt; None : default Project state None --&amp;gt; QuarantineEnter : Project quarantined, no longer in Simple API QuarantineEnter --&amp;gt; QuarantineExit : Admin clears Project, for general visibility QuarantineExit --&amp;gt; QuarantineEnter : Project re-quarantined (rare)&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Adding &lt;code&gt;LifecycleStatus&lt;/code&gt; state to the Project model helps other functions in the code make a single-point decisions, and allows for a more complex state machine to be implemented in the future. Potential states could include &#34;Archived&#34;, &#34;Deprecated&#34;, and others.&lt;/p&gt; &lt;h3 id=&#34;admin-interface&#34;&gt;Admin Interface&lt;/h3&gt; &lt;p&gt;Since the point of the implementation is to allow PyPI Admins to manage the state, and oftentimes during nights, weekends, and holidays, and from a phone web browser, I wanted to make the interface as simple as possible.&lt;/p&gt; &lt;p&gt;When developing the Admin interface, I recorded a video to share with the team, so they could see the changes in action and provide feedback.&lt;/p&gt; &lt;figure&gt; &lt;a href=&#34;https://www.loom.com/share/a472c06ab76542fca1ecaaef2a419f3d&#34;&gt; &lt;img alt=&#34;GIF of screencapture&#34; src=&#34;https://cdn.loom.com/sessions/thumbnails/a472c06ab76542fca1ecaaef2a419f3d-with-play.gif&#34;&gt; &lt;/a&gt; &lt;figcaption&gt; &lt;a href=&#34;https://www.loom.com/share/a472c06ab76542fca1ecaaef2a419f3d&#34;&gt; &lt;p&gt;Admin Interface for Quarantine 👩‍💻 - Watch Video&lt;/p&gt; &lt;/a&gt; &lt;p&gt; Note: Some of the UIs in the video may have changed since the recording, almost all data is mocked. &lt;/p&gt; &lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;As we use the admin interface more, we&#39;ll likely find areas to improve, and iterate to make the process more efficient.&lt;/p&gt; &lt;h2 id=&#34;usage&#34;&gt;Usage&lt;/h2&gt; &lt;p&gt;Since August, the Quarantine feature has been in use, with PyPI Admins marking ~140 reported projects as Quarantined.&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Quarantine Projects Admin Activity&#34; src=&#34;../../assets/2024-12-30-quarantine-verdicts.png&#34; /&gt;&lt;/p&gt; &lt;p&gt;Of these, &lt;strong&gt;only a single project&lt;/strong&gt; has exited Quarantine, others have been removed.&lt;/p&gt; &lt;p&gt;The one project cleared was a project containing obfuscated code, in violation of the PyPI &lt;a href=&#34;https://policies.python.org/pypi.org/Acceptable-Use-Policy/&#34;&gt;Acceptable Use Policy&lt;/a&gt;. The project owner corrected the violation after being contacted by PyPI Admins. I&#39;ve created some outreach templates to help with this process, and have reached out to 20+ project owners to inform them of their violation, and to provide guidance on how to correct it.&lt;/p&gt; &lt;h2 id=&#34;future-improvement-automation&#34;&gt;Future Improvement - Automation&lt;/h2&gt; &lt;p&gt;The next step in the Quarantine feature is to add the ability to automatically place a Project in Quarantine when &#34;enough credible reports&#34; are received. That&#39;s in quotes because we&#39;re still working on defining what &#34;enough&#34; and &#34;credible&#34; mean - and how to automate the process without causing undue harm to legitimate projects.&lt;/p&gt; &lt;p&gt;To date, we&#39;ve onboarded a number of security researchers, internally known as &#34;Observers&#34; to use a beta API endpoint to submit malware reports. We also allow any authenticated PyPI user to submit a malware report via a web form on a Project&#39;s page (technically a Release... but that&#39;s a different story). To prevent abuse of the quarantine system, we could place a minimum requirement of Observers reporting a given Project, as well as only consider a single non-Observer report in the calculation.&lt;/p&gt; &lt;p&gt;For example, these combinations of reports for a Project would result in a quarantined project:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;2+ Observer reports&lt;/li&gt; &lt;li&gt;1 Observer + 1 non-Observer report&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;This is only one idea so far - we could explore other combinations as they surface.&lt;/p&gt; &lt;p&gt;The idea behind Auto-Quarantine is to support the concept of receiving multiple reports &lt;strong&gt;for the same Project&lt;/strong&gt; during nights and weekends, and reduce the Project&#39;s time alive on PyPI, while preserving the ability to revert the state in a non-destructive manner in the event of a false-positive.&lt;/p&gt; &lt;p&gt;This will likely also pair with the need to add a &#34;notify admins&#34; feature. Probably a webhook to our Slack channel, so we can be notified in real-time when a Project is quarantined, and can take action as needed, as well as adding more visibility to quarantined projects in the Admin interface.&lt;/p&gt; &lt;p&gt;There&#39;s plenty of chewy bits to work on, and I&#39;m excited to see how &lt;code&gt;LifecycleStatus&lt;/code&gt; evolves, and share more about it in the future.&lt;/p&gt; &lt;!-- footnotes --&gt; &lt;div class=&#34;footnote&#34;&gt; &lt;hr /&gt; &lt;ol&gt; &lt;li id=&#34;fn:1&#34;&gt; &lt;p&gt;See &lt;a href=&#34;https://pypi.org/help/#packages&#34;&gt;https://pypi.org/help/#packages&lt;/a&gt; for more&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; title=&#34;Jump back to footnote 1 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;li id=&#34;fn:2&#34;&gt; &lt;p&gt;See &lt;a href=&#34;https://github.com/pypi/warehouse/blob/main/warehouse/packaging/models.py&#34;&gt;https://github.com/pypi/warehouse/blob/main/warehouse/packaging/models.py&lt;/a&gt; for more&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; title=&#34;Jump back to footnote 2 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;li id=&#34;fn:3&#34;&gt; &lt;p&gt;Referred to internally as &lt;code&gt;Observations(kind=&#34;is_malware&#34;)&lt;/code&gt;&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; title=&#34;Jump back to footnote 3 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;li id=&#34;fn:4&#34;&gt; &lt;p&gt;Yes, it&#39;s true, some of the database objects can be reconstructed, but it is time-consuming and tricky, used only in severe catastrophe situations.&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; title=&#34;Jump back to footnote 4 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;li id=&#34;fn:5&#34;&gt; &lt;p&gt;See &lt;a href=&#34;https://packaging.python.org/en/latest/specifications/simple-repository-api/&#34;&gt;Simple repository API - Python Packaging User Guide&lt;/a&gt; for more&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; title=&#34;Jump back to footnote 5 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;li id=&#34;fn:6&#34;&gt; &lt;p&gt;Roughly 3% of Projects in the simple index have 0 releases. Excluding these would save ~1 MB of the ~29 MB main index HTML response.&amp;#160;&lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:6&#34; title=&#34;Jump back to footnote 6 in the text&#34;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;/div&gt;</description> <link>https://blog.pypi.org/posts/2024-12-30-quarantine/</link> <pubDate>Mon, 30 Dec 2024 06:09:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_created.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2024-12-30-quarantine/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2024-12-30-quarantine.png" type="image/png" length="47987" /> </item> </channel> </rss>