<?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_updated.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>Fri, 10 Apr 2026 19:38:00 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.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 15:04:45 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.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>Welcome to the PyPI Blog</title> <author>Ee Durbin</author> <category>blogs-about-blogs</category> <category>meta</category> <category>welcome</category> <description>&lt;p&gt;Today, we&#39;re excited to launch &lt;a href=&#34;https://blog.pypi.org&#34;&gt;blog.pypi.org&lt;/a&gt;, the official blog of the Python Package Index.&lt;/p&gt; &lt;p&gt;One of the most common refrains I hear from Python community members, irrespective of if they have been around for days or years, is &lt;em&gt;&#34;I didn&#39;t realize that PyPI...&#34;&lt;/em&gt;. Followed by something along the lines of:&lt;/p&gt; &lt;!-- more --&gt; &lt;ul&gt; &lt;li&gt;Could &lt;strong&gt;do&lt;/strong&gt; that&lt;/li&gt; &lt;li&gt;Is operated with &lt;strong&gt;so&lt;/strong&gt; few resources&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;Only had &lt;strong&gt;3&lt;/strong&gt; administrators&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;&lt;/li&gt; &lt;li&gt;Needs &lt;strong&gt;more&lt;/strong&gt; help&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;That is certainly in part due to the fact that the people&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; who make PyPI &#34;happen&#34; are &lt;em&gt;humble&lt;/em&gt;, and do so with the limited time they have to commit to the project among their other responsibilities. But its also because writing is hard. Harder still when there is not a convenient tool at hand. We&#39;re hoping that a modern and extensible stack&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; for this blog will make writing about and sharing our work more frictionless.&lt;/p&gt; &lt;p&gt;The PyPI team will use this space to communicate with PyPI users, provide announcements about new features and updates, interesting technology, as well as share information and context around PyPI and related efforts of the &lt;a href=&#34;https://www.python.org/psf-landing/&#34;&gt;Python Software Foundation&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Stay tuned for more, we&#39;re very glad you&#39;re here! 💜&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;We&#39;re thrifty and picky about what we run, but &lt;a href=&#34;https://www.fastly.com/fast-forward&#34;&gt;Fastly&lt;/a&gt; is the single most important factor allowing us to get by with comparatively few resources for our backends.&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;That was until we welcomed Mike Fiedler as an admin in 2023.&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;The PyPI Administrator team is &lt;a href=&#34;https://github.com/dstufft&#34;&gt;Donald Stufft&lt;/a&gt;, &lt;a href=&#34;https://github.com/di&#34;&gt;Dustin Ingram&lt;/a&gt;, &lt;a href=&#34;https://github.com/miketheman&#34;&gt;Mike Fiedler&lt;/a&gt;, and &lt;a href=&#34;https://github.com/ewdurbin&#34;&gt;Ee Durbin&lt;/a&gt;. The PyPI Moderator team is &lt;a href=&#34;https://github.com/yeraydiazdiaz&#34;&gt;Yeray Díaz&lt;/a&gt;, &lt;a href=&#34;https://github.com/cmaureir&#34;&gt;Cristián Maureira-Fredes&lt;/a&gt;, &lt;a href=&#34;https://github.com/ewjoachim&#34;&gt;Joachim Jablon&lt;/a&gt;, &lt;a href=&#34;https://github.com/jamadden&#34;&gt;Jason Madden&lt;/a&gt;, &lt;a href=&#34;https://github.com/pradyunsg&#34;&gt;Pradyun Gedam&lt;/a&gt;, and &lt;a href=&#34;https://github.com/theacodes&#34;&gt;Stargirl Flowers&lt;/a&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;Shout-out: This blog is hosted on &lt;a href=&#34;https://readthedocs.org/&#34;&gt;Read the Docs&lt;/a&gt;, using the &lt;a href=&#34;https://pypi.org/project/mkdocs/&#34;&gt;MkDocs&lt;/a&gt; static site generator, and the &lt;a href=&#34;https://pypi.org/project/mkdocs-material/&#34;&gt;Material for MkDocs&lt;/a&gt; theme. Many thanks to the maintainers of those projects for your contributions, we&#39;re very excited to have our blog contents live side-by-side with our other documentation and code in &lt;a href=&#34;https://github.com/pypi/warehouse&#34;&gt;pypi/warehouse&lt;/a&gt;!&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;/ol&gt; &lt;/div&gt;</description> <link>https://blog.pypi.org/posts/2023-03-21-welcome-to-the-pypi-blog/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-03-21-welcome-to-the-pypi-blog/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-03-21-welcome-to-the-pypi-blog.png" type="image/png" length="50338" /> </item> <item> <title>Introducing &#39;Trusted Publishers&#39;</title> <author>Dustin Ingram</author> <category>oidc</category> <category>publishing</category> <category>security</category> <description>&lt;p&gt;Starting today, PyPI package maintainers can adopt a new, more secure publishing method that does not require long-lived passwords or API tokens to be shared with external systems.&lt;/p&gt; &lt;!-- more --&gt; &lt;h3 id=&#34;about-trusted-publishing&#34;&gt;About trusted publishing&lt;/h3&gt; &lt;p&gt;&#34;Trusted publishing&#34; is our term for using the &lt;a href=&#34;https://openid.net/connect/&#34;&gt;OpenID Connect (OIDC)&lt;/a&gt; standard to exchange short-lived identity tokens between a trusted third-party service and PyPI. This method can be used in automated environments and eliminates the need to use username/password combinations or manually generated API tokens to authenticate with PyPI when publishing.&lt;/p&gt; &lt;p&gt;Instead, PyPI maintainers can configure PyPI to trust an identity provided by a given OpenID Connect Identity Provider (IdP). This allows allows PyPI to verify and delegate trust to that identity, which is then authorized to request short-lived, tightly-scoped API tokens from PyPI. These API tokens never need to be stored or shared, rotate automatically by expiring quickly, and provide a verifiable link between a published package and its source.&lt;/p&gt; &lt;h3 id=&#34;using-trusted-publishing-with-github-actions&#34;&gt;Using trusted publishing with GitHub Actions&lt;/h3&gt; &lt;p&gt;PyPI currently supports trusted publishing with GitHub Actions, using &lt;a href=&#34;https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect&#34;&gt;their support for OpenID Connect&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;After configuring PyPI to trust a given GitHub repository and workflow, users of the PyPA&#39;s &lt;a href=&#34;https://github.com/marketplace/actions/pypi-publish&#34;&gt;&#39;pypi-publish&#39; GitHub Action&lt;/a&gt; can adopt trusted publishing by removing the &lt;code&gt;username&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; fields from their workflow configuration, and adding permissions to generate an identity token:&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-diff&#34;&gt;jobs: pypi-publish: name: upload release to PyPI runs-on: ubuntu-latest + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write steps: # retrieve your distributions here - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - with: - username: __token__ - password: ${{ secrets.PYPI_TOKEN }}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Using the PyPA&#39;s GitHub action is strongly recommended, but not required. More details on how to manually exchange tokens are available &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/using-a-publisher/#the-manual-way&#34;&gt;in our documentation&lt;/a&gt;.&lt;/p&gt; &lt;h3 id=&#34;additional-security-hardening-is-available&#34;&gt;Additional security hardening is available&lt;/h3&gt; &lt;p&gt;PyPI package maintainers can further increase the security of their release workflows by configuring trusted publishers to only release from a specific &lt;a href=&#34;https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment&#34;&gt;GitHub Actions environment&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Configuring an environment is optional, but strongly recommended: with a GitHub environment, you can apply additional restrictions to your trusted GitHub Actions workflow, such as requiring manual approval on each run by a trusted subset of repository maintainers.&lt;/p&gt; &lt;h3 id=&#34;unblocking-future-security-improvements&#34;&gt;Unblocking future security improvements&lt;/h3&gt; &lt;p&gt;In addition to making publishing more secure now, the availability of trusted publishers unblocks additional future security improvements for PyPI.&lt;/p&gt; &lt;p&gt;Configuring and using a trusted publisher provides a &#39;strong link&#39; between a project and its source repository, which can allow PyPI to verify related metadata, like the URL of a source repository for a project&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;. Additionally, publishing with a trusted publisher allows PyPI to correlate more information about where a given file was published from in a verifiable way.&lt;/p&gt; &lt;p&gt;Finally, although trusted publishers is currently limited to GitHub Actions, much of the underlying work that went into making this feature possible is generalizable and not specific to a single publisher. We&#39;re interested in supporting the ability to publish from additional services that provide OpenID Connect identities.&lt;/p&gt; &lt;h3 id=&#34;get-started-today&#34;&gt;Get started today&lt;/h3&gt; &lt;p&gt;To get started with using trusted publishers on PyPI, see our documentation here: &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/&#34;&gt;https://docs.pypi.org/trusted-publishers/&lt;/a&gt;.&lt;/p&gt; &lt;h3 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h3&gt; &lt;p&gt;Funding for this work was provided by the Google Open Source Security Team, and much of the development work was performed by &lt;a href=&#34;https://www.trailofbits.com/&#34;&gt;Trail of Bits&lt;/a&gt;, with special thanks to contributor &lt;a href=&#34;https://github.com/woodruffw&#34;&gt;William Woodruff&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Many thanks as well to &lt;a href=&#34;https://github.com/webknjaz&#34;&gt;Sviatoslav Sydorenko&lt;/a&gt;, maintainer of the PyPA&#39;s &lt;a href=&#34;https://github.com/marketplace/actions/pypi-publish&#34;&gt;&#39;pypi-publish&#39; GitHub Action&lt;/a&gt; for his quick and timely work to add support for trusted publishers in the action.&lt;/p&gt; &lt;p&gt;Finally, we want to thank all our beta testers, including GitHub staff, for working with us to ensure this feature is intuitive and useful, and for providing valuable feedback to improve this feature along the way.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;em&gt;Dustin Ingram is a maintainer of the Python Package Index.&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;Currently, information such as this are provided by the uploader and are not verified as accurate by PyPI.&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/2023-04-20-introducing-trusted-publishers/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-04-20-introducing-trusted-publishers.png" type="image/png" length="54661" /> </item> <item> <title>Introducing PyPI Organizations</title> <author>Ee Durbin</author> <category>organizations</category> <category>sustainability</category> <description>&lt;p&gt;Today, we are rolling out the first step in our plan to build financial support and long-term sustainability of the Python Packaging Index (PyPI), while simultaneously giving our users one of our most requested features: organization accounts.&lt;/p&gt; &lt;!-- more --&gt; &lt;h3 id=&#34;introducing-organizations&#34;&gt;Introducing Organizations&lt;/h3&gt; &lt;p&gt;Organizations on PyPI are self-managed teams, with their own exclusive branded web addresses. Our goal is to make PyPI easier to use for large community projects, organizations, or companies who manage multiple sub-teams and multiple packages. We’re making organizations available to community projects for free, forever, and to corporate projects for a small fee. Additional priority support agreements will be available to all paid subscribers, and all revenue will go right back into PyPI to continue building better support and infrastructure for all our users.&lt;/p&gt; &lt;h3 id=&#34;increasing-sustainability-and-support&#34;&gt;Increasing sustainability and support&lt;/h3&gt; &lt;p&gt;In the last year PyPI served 235.7 billion downloads for the 448,941 projects hosted there. This means that since the previous period, PyPI saw a 57% annual growth in download counts and bandwidth alike. Having more people using and contributing to Python every year is an fantastic problem to have, but it is one we must increase organizational capacity to accommodate.&lt;/p&gt; &lt;p&gt;Increased revenue for PyPI allows it to become a staffed platform that can respond to support requests and attend to issues in a timeframe that is significantly faster than what our excellent (but thinly spread) largely volunteer team could reasonably handle.&lt;/p&gt; &lt;h3 id=&#34;organizations-are-opt-in&#34;&gt;Organizations are opt-in&lt;/h3&gt; &lt;p&gt;We want to be very clear—these new features are completely optional. If features for larger projects don&#39;t sound like something that would be useful to you as a PyPI maintainer, then there is no obligation to create an organization and absolutely nothing about your PyPI experience will change for you.&lt;/p&gt; &lt;h3 id=&#34;a-basis-for-future-features&#34;&gt;A basis for future features&lt;/h3&gt; &lt;p&gt;We look forward to discussing what other features PyPI users would like to see tackled next. We know there are lots of ideas out there around safety, security and usability and we’re looking forward to hearing about anything that you think would benefit the community. And in the spirit of open source, &lt;a href=&#34;https://github.com/pypi/warehouse/issues&#34;&gt;we welcome your feedback&lt;/a&gt; on what&#39;s on offer so far.&lt;/p&gt; &lt;h3 id=&#34;get-started-today&#34;&gt;Get started today&lt;/h3&gt; &lt;p&gt;Both community projects (non-profits, NGO’s, hobbyists, etc) and corporate teams can &lt;a href=&#34;https://pypi.org/manage/organizations/&#34;&gt;sign up to request their organization name starting today&lt;/a&gt;. Submissions will begin seeing review and approval in the coming weeks, and corporate teams will be able to finalize their signup with billing details in May.&lt;/p&gt; &lt;h3 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h3&gt; &lt;p&gt;Organization feature development was approved by the &lt;a href=&#34;https://wiki.python.org/psf/PackagingWG&#34;&gt;Packaging Working Group&lt;/a&gt; and funded through the &lt;a href=&#34;https://www.python.org/psf-landing/&#34;&gt;Python Software Foundation&lt;/a&gt;&#39;s sponsorship program -- thanks to our &lt;a href=&#34;https://pypi.org/sponsors/&#34;&gt;sponsors&lt;/a&gt; for making this work possible.&lt;/p&gt; &lt;p&gt;We especially want to thank &lt;a href=&#34;https://www.bloomberg.com/company/values/tech-at-bloomberg/?ea-publisher=psf&#34;&gt;Bloomberg&lt;/a&gt; for funding our Packaging Project Manager, Shamika Mohanan.&lt;/p&gt; &lt;p&gt;We are also grateful for the many generous PyPI users who shared their perspectives with Shamika, which laid the foundation for these new features.&lt;/p&gt; &lt;p&gt;And thanks as well to our beta testers, including the following ✨new✨ PyPI organizations:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Python Cryptographic Authority (&lt;a href=&#34;https://pypi.org/org/pyca/&#34;&gt;https://pypi.org/org/pyca/&lt;/a&gt;)&lt;/li&gt; &lt;li&gt;The Pallets Project (&lt;a href=&#34;https://pypi.org/org/pallets/&#34;&gt;https://pypi.org/org/pallets/&lt;/a&gt;)&lt;/li&gt; &lt;li&gt;certifi (&lt;a href=&#34;https://pypi.org/org/certifi&#34;&gt;https://pypi.org/org/certifi&lt;/a&gt;)&lt;/li&gt; &lt;/ul&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;</description> <link>https://blog.pypi.org/posts/2023-04-23-introducing-pypi-organizations/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-04-23-introducing-pypi-organizations/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-04-23-introducing-pypi-organizations.png" type="image/png" length="57343" /> </item> <item> <title>Announcing the PyPI Safety &amp; Security Engineer role</title> <author>Ee Durbin</author> <category>hiring</category> <category>security</category> <description>&lt;p&gt;We are pleased to announce Amazon Web Services (AWS) as the inaugural Security Sponsor for PyPI, investing $144,000 over one year to fund key enhancements to PyPI infrastructure and operations, including the creation of a new “PyPI Safety &amp;amp; Security Engineer” role. &lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;This role builds on our existing long term partnership with AWS as one of the top sponsors of the Python Software Foundation for the last five years, which has included in-kind donations of cloud computing infrastructure and services to support PyPI. The role will complement our previously announced role for a &lt;a href=&#34;https://pyfound.blogspot.com/2023/01/the-psf-is-hiring-security-developer-in.html&#34;&gt;PSF Security Developer in Residence&lt;/a&gt; and will work closely with the person hired for that role (to be announced soon).&lt;/p&gt; &lt;p&gt;This funding also builds on &lt;a href=&#34;https://dustingram.com/articles/2021/04/14/powering-the-python-package-index-in-2021/#project-funding&#34;&gt;previously successful project-focused funding efforts&lt;/a&gt;, such as the 2018 full-stack rewrite of PyPI, the introduction of internationalization and localization for PyPI, as well as 2FA and WebAuthn support.&lt;/p&gt; &lt;p&gt;We expect this partnership to tangibly improve the experience for all PyPI users, from consumers downloading packages, to package maintainers, to large corporate teams. Some of the outcomes we are working toward over the next year include increased support for package maintainers including multi-maintainer projects, improvements to reporting infrastructure for malicious projects, as well as a reduced response time for malware reports and account recovery requests.&lt;/p&gt; &lt;p&gt;&lt;a href=&#34;https://jobs.pyfound.org/apply/CKEONredws/PyPI-Safety-Security-Engineer&#34;&gt;The job posting can be found here&lt;/a&gt;, and applications for the role are open until June 1st. Similar to existing developer-in-residence roles, the contract for this role will be for a one year period, and the PSF will be actively engaging with our sponsors and supporters to renew funding for the role in subsequent years.&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;</description> <link>https://blog.pypi.org/posts/2023-05-09-announcing-pypi-safety-and-security-engr-role/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-05-09-announcing-pypi-safety-and-security-engr-role/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-05-09-announcing-pypi-safety-and-security-engr-role.png" type="image/png" length="69077" /> </item> <item> <title>Removing PGP from PyPI</title> <author>Donald Stufft</author> <category>security</category> <description>&lt;p&gt;If you are someone who is currently uploading signatures, your package uploads will continue to succeed, but any PGP signatures will be silently ignored. If you are someone who is currently downloading PGP signatures, existing signatures &lt;em&gt;SHOULD&lt;/em&gt; continue to be available &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;, but no new signatures will be made available. The related API fields such as &lt;code&gt;has_sig&lt;/code&gt; have all been hardcoded to always be &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;Historically, PyPI has supported uploading PGP signatures alongside the release artifacts in an attempt to provide some level of package signing. However, the approach used had long standing, &lt;a href=&#34;https://caremad.io/posts/2013/07/packaging-signing-not-holy-grail/&#34;&gt;documented issues&lt;/a&gt; which had previously lead us to deemphasize the support for PGP signatures over time by removing them from the PyPI web user interface.&lt;/p&gt; &lt;p&gt;PyPI has continued to support uploading these signatures in the hope that there might be some systems out there that found them useful. Recently though, &lt;a href=&#34;https://blog.yossarian.net/2023/05/21/PGP-signatures-on-PyPI-worse-than-useless&#34;&gt;an examination of the signatures on PyPI&lt;/a&gt; has revealed to us that the current support for PGP signatures is not proving useful.&lt;/p&gt; &lt;p&gt;In the last 3 years, about 50k signatures had been uploaded to PyPI by 1069 unique keys. Of those 1069 unique keys, about 30% of them were not discoverable on major public keyservers, making it difficult or impossible to meaningfully verify those signatures. Of the remaining 71%, nearly half of them were unable to be meaningfully verified at the time of the audit (2023-05-19) &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;.&lt;/p&gt; &lt;p&gt;In other words, out of all of the unique keys that had uploaded signatures to PyPI, only 36% of them were capable of being meaningfully verified &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; at the time of audit. Even if &lt;em&gt;all&lt;/em&gt; of those signatures uploaded in that 3 year period of time were made by one of those 36% of keys that are able to be meaningfully verified, that would still represent only 0.3% of all of those files.&lt;/p&gt; &lt;p&gt;Given all of this, the continued support of uploading PGP signatures to PyPI is no longer defensible. While it doesn&#39;t represent a &lt;em&gt;massive&lt;/em&gt; operational burden to continue to support it, it does require any new features that touch the storage of files to be made aware of and capable of handling these PGP signatures, which is a non zero cost on the maintainers and contributors of PyPI.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;em&gt;Donald Stufft is a PyPI administrator and maintainer of the Python Package Index 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;For now, but they may be removed in the future.&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;These could be because the original signature was made incorrectly and never had a binding signature to a associated key identity, or because the signature was present but had since expired.&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;We use meaningfully verified to mean that the signature was valid and the key that made it was not expired and had binding identify information that could tell us if this key was the correct key.&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/2023-05-23-removing-pgp/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-05-23-removing-pgp/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-05-23-removing-pgp.png" type="image/png" length="53446" /> </item> <item> <title>PyPI was subpoenaed</title> <author>Ee Durbin</author> <category>compliance</category> <category>transparency</category> <description>&lt;p&gt;In March and April 2023, the Python Software Foundation (PSF) received three (3) subpoenas for PyPI user data. All three subpoenas were issued by the United States Department of Justice. The PSF was not provided with context on the legal circumstances surrounding these subpoenas. In total, user data related to five (5) PyPI usernames were requested.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;The data request was:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&#34;Names (including subscriber names, user names, and screen names);&#34;&lt;/li&gt; &lt;li&gt;&#34;Addresses (including mailing, residential addresses, business addresses, and email addresses);&#34;&lt;/li&gt; &lt;li&gt;&#34;Connection records;&#34;&lt;/li&gt; &lt;li&gt;&#34;Records of session times and durations, and the temporarily assigned network address (such as Internet Protocol addresses) associated with those sessions;&#34;&lt;/li&gt; &lt;li&gt;&#34;Length of service (including start date) and type of services utilized;&#34;&lt;/li&gt; &lt;li&gt;&#34;Telephone or instrument numbers (including the registration Internet Protocol address);&#34;&lt;/li&gt; &lt;li&gt;&#34;Means and source of payment of any such services (including any credit card or bank account number) and billing records;&#34;&lt;/li&gt; &lt;li&gt;&#34;Records of all Python Package Index (PyPI) packages uploaded by...&#34; given usernames&lt;/li&gt; &lt;li&gt;&#34;IP download logs of any Python Package Index (PyPI) packages uploaded by...&#34; given usernames&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;The privacy of PyPI users is of utmost concern to PSF and the PyPI Administrators, and we are committed to protecting user data from disclosure whenever possible. In this case, however, PSF determined with the advice of counsel that our only course of action was to provide the requested data. I, as Director of Infrastructure of the Python Software Foundation, fulfilled the requests in consultation with PSF&#39;s counsel.&lt;/p&gt; &lt;p&gt;We have waited for the string of subpoenas to subside, though we were committed from the beginning to write and publish this post as a matter of transparency, and as allowed by the lack of a non-disclosure order associated with the subpoenas received in March and April 2023.&lt;/p&gt; &lt;h2 id=&#34;next-steps&#34;&gt;Next Steps&lt;/h2&gt; &lt;p&gt;PyPI and the PSF are committed to the freedom, security, and privacy of our users.&lt;/p&gt; &lt;p&gt;This process has offered time to revisit our current data and privacy standards, which are minimal, to ensure they take into account the varied interests of the Python community. Though we collect very little personal data from PyPI users, any unnecessarily held data are still subject to these kinds of requests in addition to the baseline risk of data compromise via malice or operator error.&lt;/p&gt; &lt;p&gt;As a result we are currently developing new data retention and disclosure policies. These policies will relate to our procedures for future government data requests, how and for what duration we store personally identifiable information such as user access records, and policies that make these explicit for our users and community.&lt;/p&gt; &lt;p&gt;Please continue to watch this blog for related announcements as policies are finalized, published, and implemented.&lt;/p&gt; &lt;hr /&gt; &lt;h2 id=&#34;details&#34;&gt;Details&lt;/h2&gt; &lt;p&gt;In order to provide as much transparency as possible, the following will detail the shape of and extent of the data that was contained in the responses to these subpoenas.&lt;/p&gt; &lt;p&gt;We will not be releasing the usernames involved publicly or to the users themselves.&lt;/p&gt; &lt;h3 id=&#34;1-names-including-subscriber-names-user-names-and-screen-names&#34;&gt;1) Names (including subscriber names, user names, and screen names);&lt;/h3&gt; &lt;p&gt;These were confirmed via our database records&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-sql&#34;&gt;select id, username, name from users where username = &#39;{USERNAME}&#39;;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Returning: &lt;pre class=&#34;highlight&#34;&gt;&lt;code&gt;id | UUID for USERNAME username | PyPI username name | Display name for user&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt; &lt;p&gt;And are also publicly available at &lt;code&gt;https://pypi.org/user/{USERNAME}/&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;PyPI allows users to delete their accounts.&lt;/p&gt; &lt;p&gt;PyPI does not allow for the &lt;code&gt;username&lt;/code&gt; field to be changed without admin intervention, and no such intervention has occurred for the users in question. If they had, we would have provided records of those changes.&lt;/p&gt; &lt;p&gt;PyPI does allow for user to update their display names and keeps no record of the history of these changes.&lt;/p&gt; &lt;h3 id=&#34;2-addresses-including-mailing-residential-addresses-business-addresses-and-email-addresses&#34;&gt;2) Addresses (including mailing, residential addresses, business addresses, and email addresses);&lt;/h3&gt; &lt;p&gt;PyPI only stores email addresses for individual users. No physical addresses are stored. Organization accounts who have signed up for billing (not yet live) will be required to provide a billing address that validates to their payment method.&lt;/p&gt; &lt;p&gt;These were sourced from our database records and is private to PyPI.&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-sql&#34;&gt;select email, user_id from user_emails where user_id =&#39;{UUID for USERNAME}&#39;;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Returning: &lt;pre class=&#34;highlight&#34;&gt;&lt;code&gt;email | An email address user_id | UUID for USERNAME&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt; &lt;p&gt;PyPI allows for users to add and remove email addresses without admin intervention. Records of these changes are kept, and no such changes were observed in our records for the usernames in question. If they had, we would have provided records of those changes.&lt;/p&gt; &lt;h3 id=&#34;3-connection-records&#34;&gt;3. Connection records&lt;/h3&gt; &lt;h4 id=&#34;3a-project-events&#34;&gt;3a. Project events&lt;/h4&gt; &lt;p&gt;PyPI retains records of all changes to projects on the index, and has since &lt;code&gt;2002-11-01 17:11:36 UTC&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;These were confirmed via our database records&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-sql&#34;&gt;select * from journals where submitted_by=&#39;{USERNAME}&#39; order by submitted_date;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Returning: &lt;pre class=&#34;highlight&#34;&gt;&lt;code&gt;id | An auto incrementing integer representing the &#34;Serial&#34; name | Name of a PyPI Project version | Version of a PyPI Release if relevant action | Description of the action taken against the Project/Release submitted_date | ISO-8601 datetime in UTC submitted_by | PyPI Username submitted_from | IP Address&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt; &lt;p&gt;and with the exception of the &lt;code&gt;submitted_by&lt;/code&gt; (PyPI username) and &lt;code&gt;submitted_from&lt;/code&gt; (IP Address) columns are publicly available via our &lt;a href=&#34;https://warehouse.pypa.io/api-reference/xml-rpc.html#changelog-since-with-ids-false&#34;&gt;XMLRPC API&lt;/a&gt;.&lt;/p&gt; &lt;h4 id=&#34;3b-user-events&#34;&gt;3b. User events&lt;/h4&gt; &lt;p&gt;PyPI retains records of critical user events including account creation, emails sent, email address changes, logins, and login failures. See &lt;a href=&#34;https://github.com/pypi/warehouse/blob/9738ebb2ffcee91a935a6a11b224575aaf02a878/warehouse/events/tags.py#L61-L106&#34;&gt;this list&lt;/a&gt; for the comprehensive set of events recorded.&lt;/p&gt; &lt;p&gt;These were sourced from our database records&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-sql&#34;&gt;select * from user_events where source_id = &#39;{UUID for USERNAME}&#39; order by time desc;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Returning: &lt;pre class=&#34;highlight&#34;&gt;&lt;code&gt;id | UUID of the event source_id | UUID for USERNAME tag | EventTag time | ISO-8601 datetime in UTC ip_address_string | IP Address additional | JSONB metadata about the event ip_address_id | UUID of associated IPAddress object&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt; &lt;p&gt;and are private to PyPI.&lt;/p&gt; &lt;h3 id=&#34;4-records-of-session-times-and-durations-and-the-temporarily-assigned-network-address-such-as-internet-protocol-addresses-associated-with-those-sessions&#34;&gt;4. Records of session times and durations, and the temporarily assigned network address (such as Internet Protocol addresses) associated with those sessions;&lt;/h3&gt; &lt;p&gt;PyPI does not record session durations.&lt;/p&gt; &lt;p&gt;Session creation (Login) times were provided as a synopsis of the data in 3b.&lt;/p&gt; &lt;p&gt;Sessions are not created for uploads, but the associated login events for project uploads were provided as a synopsis of the data in 3a.&lt;/p&gt; &lt;h3 id=&#34;5-length-of-service-including-start-date-and-type-of-services-utilized&#34;&gt;5. Length of service (including start date) and type of services utilized;&lt;/h3&gt; &lt;p&gt;PyPI retains records of the date that a user account was created, as well as the last time it was successfully logged in by any method (web UI or command line tool for upload).&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-sql&#34;&gt;select date_joined, last_login from users where username = {USERNAME}&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Returning: &lt;pre class=&#34;highlight&#34;&gt;&lt;code&gt;date_joined | ISO-8601 datetime in UTC last_login | ISO-8601 datetime in UTC&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt; &lt;p&gt;These were sourced from our database records and are private to PyPI.&lt;/p&gt; &lt;p&gt;Types of service utilized are &#34;standard&#34; to PyPI and include the ability to create projects, releases, and distribution files for downloads.&lt;/p&gt; &lt;h3 id=&#34;6-telephone-or-instrument-numbers-including-the-registration-internet-protocol-address&#34;&gt;6. Telephone or instrument numbers (including the registration Internet Protocol address);&lt;/h3&gt; &lt;p&gt;A synopsis of all IP Addresses for each username from previous records were shared.&lt;/p&gt; &lt;p&gt;These were sourced from our database records and are private to PyPI.&lt;/p&gt; &lt;h3 id=&#34;7-means-and-source-of-payment-of-any-such-services-including-any-credit-card-or-bank-account-number-and-billing-records&#34;&gt;7. Means and source of payment of any such services (including any credit card or bank account number) and billing records;&lt;/h3&gt; &lt;p&gt;PyPI has no cost to use for individual users and no such payment records or billing records exist.&lt;/p&gt; &lt;h3 id=&#34;8-records-of-all-python-package-index-pypi-packages-uploaded-by-the-given-usernames&#34;&gt;8. Records of all Python Package Index (PyPI) packages uploaded by the given usernames&lt;/h3&gt; &lt;p&gt;A list of all past and any current projects associated with each username was provided.&lt;/p&gt; &lt;p&gt;These were sourced from our database records and for past projects, are private to PyPI.&lt;/p&gt; &lt;h3 id=&#34;9-ip-download-logs-of-any-python-package-index-pypi-packages-uploaded-by-the-given-usernames&#34;&gt;9. IP download logs of any Python Package Index (PyPI) packages uploaded by the given usernames&lt;/h3&gt; &lt;p&gt;PyPI does not retain download logs for packages which include IP addresses. Download logs are processed by a pipeline which includes GeoIP information reported by our CDN only.&lt;/p&gt; &lt;p&gt;These records were sourced from the &lt;a href=&#34;https://docs.pypi.org/api/bigquery/&#34;&gt;Google BigQuery Public dataset&lt;/a&gt; with the following queries:&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-sql&#34;&gt;SELECT * FROM `bigquery-public-data.pypi.file_downloads` WHERE project IN ({LIST OF PROJECT NAMES FROM 8}) AND timestamp &amp;gt; &#39;{START OF PERIOD IN QUESTION}&#39;;&lt;/code&gt;&lt;/pre&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;</description> <link>https://blog.pypi.org/posts/2023-05-24-pypi-was-subpoenaed/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-05-24-pypi-was-subpoenaed/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-05-24-pypi-was-subpoenaed.png" type="image/png" length="60856" /> </item> <item> <title>Securing PyPI accounts via Two-Factor Authentication</title> <author>Donald Stufft</author> <category>2fa</category> <category>security</category> <description>&lt;p&gt;One of the key security promises that PyPI makes is that when you&#39;re downloading something, that only the people associated with that project are going to be able to upload, delete, or otherwise modify a project. That when you look at that project and see that it is owned by someone that you trust, that you can be assured that nobody else is making changes to that package on PyPI.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;This promise is predicated on the security of each and every individual account on PyPI used to create and maintain a Python project. In the past we&#39;ve taken steps to safeguard these accounts by &lt;a href=&#34;https://caremad.io/posts/2018/08/pypi-compromised-passwords/&#34;&gt;blocking compromised passwords&lt;/a&gt;, strong 2FA support using &lt;a href=&#34;https://github.com/pypi/warehouse/pull/5567&#34;&gt;TOTP&lt;/a&gt; and &lt;a href=&#34;https://github.com/pypi/warehouse/pull/5795&#34;&gt;WebAuthN&lt;/a&gt;, &lt;a href=&#34;https://pypi.org/help/#apitoken&#34;&gt;support for API tokens with offline attenuation&lt;/a&gt;, &lt;a href=&#34;https://pypi.org/security-key-giveaway/&#34;&gt;enrolling the most downloaded projects into mandatory 2FA&lt;/a&gt;, and &lt;a href=&#34;https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/&#34;&gt;enabling short lived tokens for upload&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Today, as part of that long term effort to secure the Python ecosystem, we are announcing that &lt;em&gt;every&lt;/em&gt; account that maintains any project or organization on PyPI will be required to enable 2FA on their account by the end of 2023.&lt;/p&gt; &lt;p&gt;Between now and the end of the year, PyPI will begin gating access to certain site functionality based on 2FA usage. In addition, we may begin selecting certain users or projects for early enforcement.&lt;/p&gt; &lt;h2 id=&#34;what-can-i-do-to-prepare&#34;&gt;What can I do to prepare?&lt;/h2&gt; &lt;p&gt;The most important things you can do to prepare are to enable 2FA for your account as soon as possible, either with a &lt;a href=&#34;https://pypi.org/help/#utfkey&#34;&gt;security device&lt;/a&gt; (preferred) or an &lt;a href=&#34;https://pypi.org/help/#totp&#34;&gt;authentication app&lt;/a&gt; and to switch to using either &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/&#34;&gt;Trusted Publishers&lt;/a&gt; (preferred) or &lt;a href=&#34;https://pypi.org/help/#apitoken&#34;&gt;API tokens&lt;/a&gt; to upload to PyPI.&lt;/p&gt; &lt;h2 id=&#34;why-use-2fa&#34;&gt;Why Use 2FA?&lt;/h2&gt; &lt;p&gt;Account takeover attacks typically stem from someone using an insecure password: perhaps it was easy to guess, or it was reused and appeared in a breach. With that insecure password, an attacker is able to gain control over a maintainers account and begin to take actions as if they were that user.&lt;/p&gt; &lt;p&gt;This is particularly problematic on a site like PyPI, where the actions that a person can take include releasing software that might be used by people world wide, allowing an attacker to install and execute software on unsuspecting user&#39;s computers. &lt;a href=&#34;https://python-security.readthedocs.io/pypi-vuln/index-2022-05-24-ctx-domain-takeover.html&#34;&gt;Account takeover attacks have been previously used to compromise PyPI users in this way&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Two-factor authentication immediately neutralizes the risk associated with a compromised password. If an attacker has someone&#39;s password, that is no longer enough to give them access to that account.&lt;/p&gt; &lt;h2 id=&#34;why-every-project-or-organization-maintainer&#34;&gt;Why every project or organization maintainer?&lt;/h2&gt; &lt;p&gt;There&#39;s two ways to think about this question:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Why every project and organization maintainer instead of just some subset of them (based on popularity, purpose, whether that user uses their account, etc)?&lt;/li&gt; &lt;li&gt;Why only maintainers and not every single user?&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Not every account on PyPI has the same value to an attacker. An account with access to the most downloaded project on PyPI can be used to attack far more people than an account with access to the least downloaded project.&lt;/p&gt; &lt;p&gt;However, it only takes one compromised project in someone&#39;s dependency set to compromise their computer. The attacker doesn&#39;t care if they get you from a widely used or a niche project, just that they got you. Even worse, once compromised, an attacker can extend that attack to attack &lt;em&gt;other&lt;/em&gt; systems, including other projects on PyPI that the now compromised person maintains.&lt;/p&gt; &lt;p&gt;Given that it only takes one compromised project, no matter how many downloads it gets &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;, to compromise someone we want to ensure that every project is being protected by 2FA.&lt;/p&gt; &lt;p&gt;On the flipside, an account without access to any project cannot be used to attack anyone &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; so it is a very low value target.&lt;/p&gt; &lt;p&gt;We recognize that enabling 2FA for an account imposes a non zero cost both for the owner of that account &lt;em&gt;and&lt;/em&gt; for PyPI &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;, so forcing that on accounts that cannot affect anyone but themselves is not an effective use of limited resources. In addition, from a practical view, the standard 2FA flow that most people are used to and that PyPI implements also involves adding a 2FA token to an existing account rather than forcing it to be part of the registration flow.&lt;/p&gt; &lt;p&gt;Our expectation is that for users who currently are not a project maintainer or organization member, the ultimate experience will be whenever they attempt to take some action that would require them to add 2FA (creating a new project, accepting an invite, making an organization, etc) they will be prompted to add 2FA to their account before they can proceed.&lt;/p&gt; &lt;h2 id=&#34;why-now&#34;&gt;Why now?&lt;/h2&gt; &lt;p&gt;The direction of many projects in or serving the Open Source community in the last 5-10 years has been an increasing focus on supply chain security, preventing attacks that are being delivered through the &#34;supply chain&#34;, namely the services and tooling used to create and consume software.&lt;/p&gt; &lt;p&gt;In July of 2022, we announced &lt;a href=&#34;https://pypi.org/security-key-giveaway/&#34;&gt;a security key giveway&lt;/a&gt; in conjunction with a plan to begin mandating 2FA for the top 1% of projects on PyPI by download count.&lt;/p&gt; &lt;p&gt;The initial announcement of that mandate to the top 1% of projects and the related giveaway was met with mixed reactions, ranging from people applauding the effort to people deciding to distribute their code on places other than PyPI. We planned to limit this to the projects in the top 1% of downloads because those are likely he highest value targets for an attacker, and because we were concerned about the impact of such a mandate on both the maintainers of projects on PyPI, and on the support burden of the PyPI team itself.&lt;/p&gt; &lt;p&gt;At that time last year, we did not have any plans or expectations on widening our net of users that would fall under that mandate, other than would occur naturally as projects rose in popularity to be part the top 1%.&lt;/p&gt; &lt;p&gt;However, in the year since then a few things have changed.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;We&#39;ve gotten a lot more confident in our 2FA implementation and in what the impact to enabling it is for both people publishing to PyPI, and to the PyPI team itself.&lt;/li&gt; &lt;li&gt;We&#39;ve shipped features like &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/&#34;&gt;Trusted Publishing&lt;/a&gt; that help remove some of the overheard of 2FA has on publishing (by far the most common action users do).&lt;/li&gt; &lt;li&gt;GitHub has furthered it&#39;s &lt;a href=&#34;https://github.blog/2022-05-04-software-security-starts-with-the-developer-securing-developer-accounts-with-2fa/&#34;&gt;plans to mandate 2FA for all contributors&lt;/a&gt; on their platform, making more people already (or soon will be) prepared to cope with the requirements of having 2FA.&lt;/li&gt; &lt;li&gt;The PSF has received funding to hire a &lt;a href=&#34;https://blog.pypi.org/posts/2023-05-09-announcing-pypi-safety-and-security-engr-role/&#34;&gt;PyPI Safety and Security Engineer&lt;/a&gt;. While the role is not meant to purely handle support requests, having dedicated focus on the overall security posture, as well as the implementation specifics will improve PyPI as a whole for both users and project maintainers, and will help alleviate some of the pressures on PyPI&#39;s volunteers.&lt;/li&gt; &lt;li&gt;The workload to support end users relies heavily on a very small group of volunteers. When an user account report is seen by our trusted admins, we have to take time to properly investigate. These are often reported as an emergency, red-alert-level urgency. By mandating 2FA for project maintainers, the likelihood of account takeovers drop significantly, reserving the emergency status for truly extraordinary circumstances. Account recovery becomes part of normal routine support efforts instead of admin-level urgency.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;All of these together help lead us to the conclusion that we can widen our mandate to &lt;em&gt;all&lt;/em&gt; project maintainers on PyPI, while minimizing the impact on both project maintainers and PyPI administrators, and to do so in a way that improves the sustainability of PyPI and the wider Python ecosystem.&lt;/p&gt; &lt;h2 id=&#34;isnt-supply-chain-security-a-corporate-concern&#34;&gt;Isn&#39;t supply chain security a corporate concern?&lt;/h2&gt; &lt;p&gt;There are some people who believe that efforts to improve supply chain security benefits only corporate or business users, and that individual developers should not be asked to take on a uncompensated burden for their benefit.&lt;/p&gt; &lt;p&gt;We believe this is shortsighted.&lt;/p&gt; &lt;p&gt;A compromise in the supply chain can be used to attack individual developers the same as it able to attack corporate and business users. In fact, we believe that individual developers, are in a &lt;em&gt;more&lt;/em&gt; vulnerable position than corporate and business users. While businesses are generally able to hire staff and devote resources to vetting their dependencies, individual developers generally are not, and must expend their own limited free time to do so &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;To make matters worse for the individual, in the case of a compromise a business is more likely going to have experts available to them to detect and remediate the compromise, while the individual has to do this on their own. At the extreme ends, businesses often have insurance to compensate them for any losses incurred while the individual almost always does not.&lt;/p&gt; &lt;p&gt;We recognize that supply chain security effects &lt;em&gt;everyone&lt;/em&gt;, no matter how big or how small they are, and we are dedicated to protecting &lt;em&gt;all&lt;/em&gt; our users.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;em&gt;Donald Stufft is a PyPI administrator and maintainer of the Python Package Index 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;&lt;em&gt;Technically&lt;/em&gt; a project with 0 downloads is effectively the same as a non-existent project, but it&#39;s easier to draw the line between non-existent and existent than it is to draw the line between 0 and 1 downloads. This is particularly true on PyPI, where a large network of mirrors and scanners mean that &lt;em&gt;no&lt;/em&gt; projects truly get downloaded exactly 0 times.&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;Except maybe the account owner themselves, by denying them access to their account.&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;For end users it forces them to purchase some kind of hardware token &lt;em&gt;OR&lt;/em&gt; to use some sort of TOTP application. In both cases it forces them to keep track of something else besides their password and changes the login flow from what they are used to. For PyPI it increases the chance that someone may get locked out of their account, requiring intervention by administrators.&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;Not for nothing, but PyPI is also an Open Source project, run largely by volunteers, and cleaning up after a compromise on PyPI is something that affects those volunteers significantly.&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;/ol&gt; &lt;/div&gt;</description> <link>https://blog.pypi.org/posts/2023-05-25-securing-pypi-with-2fa/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-05-25-securing-pypi-with-2fa/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-05-25-securing-pypi-with-2fa.png" type="image/png" length="71926" /> </item> <item> <title>Reducing Stored IP Data in PyPI</title> <author>Mike Fiedler</author> <category>security</category> <category>transparency</category> <description>&lt;p&gt;Hi there! I&#39;m Mike, the newest member of the PyPI admin team. Nice to meet you!&lt;/p&gt; &lt;h2 id=&#34;tldr&#34;&gt;TL;DR&lt;/h2&gt; &lt;p&gt;We&#39;ve been working on reducing the amount of IP address data we store, and we&#39;re making progress.&lt;/p&gt; &lt;!-- more --&gt; &lt;h2 id=&#34;whats-this-about&#34;&gt;What&#39;s this about?&lt;/h2&gt; &lt;p&gt;If you&#39;ve read some of the other blogs here, you may have noticed that we&#39;ve been working on a number of security and privacy improvements.&lt;/p&gt; &lt;p&gt;A few months ago we started exploring what it would take to remove the concept of IP addresses from our stack, and retain the ability to safely manage the platform.&lt;/p&gt; &lt;p&gt;Some places of where IP data was historically used:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;web access logs&lt;/li&gt; &lt;li&gt;user events (login,. logout, password reset, et al)&lt;/li&gt; &lt;li&gt;project events (creation, new releases, new file uploads, yanks, et al)&lt;/li&gt; &lt;li&gt;organization/team membership events (&lt;a href=&#34;../2023-04-23-introducing-pypi-organizations/&#34;&gt;new!&lt;/a&gt;)&lt;/li&gt; &lt;li&gt;journal entries (private to PyPI admins)&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Security is a spectrum - where on one extreme, it&#39;s the wild west, no security. On the other extreme, everything is locked down, impossible to use. We constantly have to balance the needs and desires of our users with the needs of running a sustainable, trusted platform.&lt;/p&gt; &lt;p&gt;A similar mindset can be applied to privacy - where we must strike the balance between providing a manageable service, and protecting the privacy of our users.&lt;/p&gt; &lt;p&gt;The two main approaches we&#39;ve pursued in the short term are:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Evaluate whether we need to store IP data at all&lt;/li&gt; &lt;li&gt;Whenever possible, use alternatives to IP data&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;I&#39;ll provide a couple of examples demonstrating the above.&lt;/p&gt; &lt;h2 id=&#34;do-we-need-the-ip-data&#34;&gt;Do we need the IP data?&lt;/h2&gt; &lt;p&gt;As we evaluated the different places we stored IP data, we learned that our Journal entries (similar to an append-only transaction log) were never exposed beyond admin users, and even then, used for admin display only.&lt;/p&gt; &lt;p&gt;We never share that via API, or used it for operational purposed. So we audited the code, removed calls to stop writing the column, and dropped it.&lt;/p&gt; &lt;p&gt;Woohoo!&lt;/p&gt; &lt;p&gt;Other places where we currently still need IP data include rate limiting, and fallbacks until we have backfilled the IP data with hashes and geo data. Our modern approach has evolved from using the IP data at display time to find the relevant geo data, to storing the geo data directly in the database.&lt;/p&gt; &lt;p&gt;Another use case is for handling abuse - we need to be able to identify the source of abuse, and take action to prevent it. We&#39;re thinking about how to manage that without storing IP data, but we&#39;re not there yet.&lt;/p&gt; &lt;h2 id=&#34;alternatives-to-ip-data&#34;&gt;Alternatives to IP data&lt;/h2&gt; &lt;p&gt;For the other places where we stored IP data, we asked ourselves - could we use an alternative?&lt;/p&gt; &lt;p&gt;We can&#39;t store what we don&#39;t see, so we explored what we could do to reduce the amount of IP data we see.&lt;/p&gt; &lt;p&gt;Pretty much every request to PyPI is served via our CDN partner, Fastly. They provide a number of features, including the ability to &lt;a href=&#34;https://docs.fastly.com/en/guides/adding-or-modifying-headers-on-http-requests-and-responses&#34;&gt;add custom headers&lt;/a&gt;. We leverage this ability already for a number of things, like informing the warehouse code of the inbound encoding of the request or language.&lt;/p&gt; &lt;p&gt;We explored whether we could use this to add a hash of the IP address, and use that instead of the IP address itself. Fastly can also provide some basic geographic info about the IP address, which served our purpose of showing the user where they had connected from.&lt;/p&gt; &lt;p&gt;Using this approach, we have Fastly pass along an already-hashed IP address, as well as the geographic data, to our backend, and store those for later use.&lt;/p&gt; &lt;p&gt;Another spot we identified was web access logs. We don&#39;t need real IP addresses there, as we rarely use them for anything other than low-level debugging, so a stable, hashed value serves the purpose of correlating requests.&lt;/p&gt; &lt;p&gt;For the requests that Fastly doesn&#39;t serve, we&#39;re already hashing the IP address ourselves prior to storage, so we could &#34;see&#34; the IP address briefly, but we try to avoid storing it. We don&#39;t get get the geo data for these requests, we&#39;re thinking of creative and sustainable solutions already.&lt;/p&gt; &lt;h2 id=&#34;questions-and-answers&#34;&gt;Questions and Answers&lt;/h2&gt; &lt;p&gt;I tried to think up some questions you might have, and answer them here. If you have more, please feel free to reach out to us!&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Is a hashed IP address secure?&lt;/p&gt; &lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; It&#39;s more secure than a plain IP address, for sure. We apply a &lt;a href=&#34;https://en.wikipedia.org/wiki/Salt_(cryptography)&#34;&gt;salt&lt;/a&gt; (known value) to the IP address before hashing it. It&#39;s not a perfect solution, but it&#39;s a step in the right direction.&lt;/p&gt; &lt;p&gt;The hash is non-reversible, but since the known address space is relatively small, it&#39;s possible to brute force the hash to determine the original IP address. By applying a salt, we require someone to possess &lt;strong&gt;both&lt;/strong&gt; the salt and the hashed IP addresses to brute force the value. Our salt is not stored in the database while the hashed IP addresses are, we protect against leaks revealing this information.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Is this a response to the subpoenas?&lt;/p&gt; &lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; No, we started exploring this &lt;a href=&#34;https://github.com/pypi/warehouse/issues/8158&#34;&gt;back in 2020&lt;/a&gt;, with a long term goal to increase end-user security, while retaining the ability to effectively steer the platform. We picked it up recently as we explored our CDN partner&#39;s options for geo IP data.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Are we done?&lt;/p&gt; &lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; Nope! Security is an ongoing journey, and we&#39;re making strong strides to accomplish this goal. We still have some work to do to replace IP data in our models, after we&#39;ve backfilled our models with the hashed IP data and relevant geo data, and clean up some of the code.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What&#39;s next?&lt;/p&gt; &lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; I can&#39;t predict every future step we&#39;re likely to take, but some things we&#39;re considering:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Reevaluate the need for IP data in Event history &lt;strong&gt;forever&lt;/strong&gt;, remove it after a period of time&lt;/li&gt; &lt;li&gt;Explore whether we can use a CDN for all requests&lt;/li&gt; &lt;li&gt;Determine if there&#39;s a better mechanism than Journal Entries, and replace them&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;We believe the steps we&#39;re taking are in the right direction, and we&#39;re excited to share our progress with you. Hopefully this enriches your understanding of the work we&#39;re doing, in support of maintaining a secure, trusted platform.&lt;/p&gt; &lt;p&gt;Thanks for reading!&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;em&gt;Mike Fiedler is a PyPI administrator and maintainer of the Python Package Index since 2022.&lt;/em&gt;&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2023-05-26-reducing-stored-ip-data/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-05-26-reducing-stored-ip-data/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-05-26-reducing-stored-ip-data.png" type="image/png" length="59649" /> </item> <item> <title>Enforcement of 2FA for upload.pypi.org begins today</title> <author>Ee Durbin</author> <category>2fa</category> <category>security</category> <description>&lt;p&gt;Beginning today, all uploads from user accounts with 2FA enabled will be required to use an &lt;a href=&#34;https://pypi.org/help/#apitoken&#34;&gt;API Token&lt;/a&gt; or &lt;a href=&#34;https://docs.pypi.org/trusted-publishers/&#34;&gt;Trusted Publisher&lt;/a&gt; configuration in place of their password.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;This change has &lt;a href=&#34;https://github.com/pypi/warehouse/issues/7265&#34;&gt;been planned&lt;/a&gt; since 2FA was rolled out in 2019. In &lt;a href=&#34;https://github.com/pypi/warehouse/pull/10836&#34;&gt;February of 2022&lt;/a&gt; we began notifying users on upload that this change was coming.&lt;/p&gt; &lt;p&gt;If you have 2FA enabled and have been using only your password to upload, the following email is likely familiar to you:&lt;/p&gt; &lt;figure&gt; &lt;p&gt;&lt;img alt=&#34;Sample notice email&#34; src=&#34;../../assets/2023-06-01-2fa-notice-email.png&#34; /&gt; &lt;/p&gt; &lt;figcaption&gt; A sample notice email sent when users with 2FA enabled upload using only their password. &lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;Initially, we intended for this notice to live for six months before we began enforcement.&lt;/p&gt; &lt;p&gt;However, some valid concerns were raised regarding the use of user-scoped API tokens for new project creation.&lt;/p&gt; &lt;p&gt;With the &lt;a href=&#34;../2023-04-20-introducing-trusted-publishers/&#34;&gt;introduction of Trusted Publishers&lt;/a&gt; PyPI now provides a way for users to publish &lt;strong&gt;new&lt;/strong&gt; projects without provisioning a user-scoped token, and to continue publishing without ever provisioning a long lived API token whatsoever.&lt;/p&gt; &lt;p&gt;Given this, and our &lt;a href=&#34;../2023-05-25-securing-pypi-with-2fa/&#34;&gt;commitment to further rolling out 2FA across PyPI&lt;/a&gt;, we are now enforcing this policy.&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;</description> <link>https://blog.pypi.org/posts/2023-06-01-2fa-enforcement-for-upload/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-06-01-2fa-enforcement-for-upload/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-06-01-2fa-enforcement-for-upload.png" type="image/png" length="76445" /> </item> <item> <title>Announcing the launch of PyPI Malware Reporting and Response project</title> <author>Shamika Monahan</author> <category>security</category> <description>&lt;p&gt;We are pleased to announce that the PSF has received funding from the &lt;a href=&#34;https://cset.georgetown.edu/&#34;&gt;Center for Security and Emerging Technology&lt;/a&gt; (CSET) to develop and improve the infrastructure for malware reporting and response on PyPI. This project will be executed over the coming year.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;Currently, malware reports are submitted to PyPI admins by email before being manually triaged and responded to. There is an opportunity for improvement in streamlining the report submission process and the tools used to triage and respond to them. The current process cannot scale easily or handle duplication of reports. It is not easy to measure time to remediation and is currently impossible to implement automated takedown of threats.&lt;/p&gt; &lt;p&gt;This project has the following aims:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Develop an API that allows malware reporting&lt;/li&gt; &lt;li&gt;Extend PyPI admin tools to view, collate and handle security reports&lt;/li&gt; &lt;li&gt;Collect metadata as required and identify trusted reporters&lt;/li&gt; &lt;li&gt;Define metrics that allow us to define good reporting practices and time to handle a security issue&lt;/li&gt; &lt;li&gt;Define the criteria for automated consensus based takedown and soft-deletes of packages&lt;/li&gt; &lt;li&gt;Highlight trusted reporters and report quality&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;As PyPI is an integral part of the Python ecosystem, this project is crucial in ensuring the security of over 450,000 packages that are trusted by millions of Python developers. Over the next few weeks, we will be working with security reporters to identify key elements that should be supported by the API and useful metrics that would add value to PyPI security reporting. If you or your colleagues are currently performing malware analysis of PyPI uploads, we would love to hear from you at &lt;a href=&#34;https://forms.gle/ixRoNJEPVNekFN7H7&#34;&gt;https://forms.gle/ixRoNJEPVNekFN7H7&lt;/a&gt;.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;em&gt;Shamika Mohanan is the Packaging Project Manager at the PSF since 2021.&lt;/em&gt;&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2023-06-22-malware-detection-project/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-06-22-malware-detection-project/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-06-22-malware-detection-project.png" type="image/png" length="60440" /> </item> <item> <title>Deprecation of bdist_egg uploads to PyPI</title> <author>Ee Durbin</author> <category>deprecation</category> <description>&lt;p&gt;&lt;a href=&#34;https://peps.python.org/pep-0715/&#34;&gt;PEP 715&lt;/a&gt;, deprecating &lt;code&gt;bdist_egg&lt;/code&gt;/&lt;code&gt;.egg&lt;/code&gt; uploads to PyPI has been &lt;a href=&#34;https://discuss.python.org/t/pep-715-disabling-bdist-egg-distribution-uploads-on-pypi/27610/13&#34;&gt;accepted&lt;/a&gt;. We&#39;ll begin the process of implementing this today.&lt;/p&gt; &lt;p&gt;Please note that this does &lt;strong&gt;NOT&lt;/strong&gt; remove any existing uploaded eggs from PyPI.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;The deprecation timeline is as follows:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Today, June 26, 2023: All maintainers of projects which have uploaded one or more eggs since January 1, 2023 will receive a one-time email informing them of this change.&lt;/li&gt; &lt;li&gt;Today, June 26, 2023: Each upload of an egg to PyPI will result in a notice being sent to all Owners and Maintainers for the project.&lt;/li&gt; &lt;li&gt;August 1, 2023: Uploads of eggs will be &lt;a href=&#34;https://i.kym-cdn.com/photos/images/original/001/402/192/398.jpg&#34;&gt;&lt;strong&gt;rejected&lt;/strong&gt;&lt;/a&gt; by PyPI.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;You can read more detailed rationale in &lt;a href=&#34;https://peps.python.org/pep-0715/#rationale&#34;&gt;PEP 715&lt;/a&gt;. Thanks to contributor &lt;a href=&#34;https://blog.yossarian.net&#34;&gt;William Woodruff&lt;/a&gt; for his work to author and propose PEP 715, as well as support the rollout of the implementation.&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;</description> <link>https://blog.pypi.org/posts/2023-06-26-deprecate-egg-uploads/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-06-26-deprecate-egg-uploads/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-06-26-deprecate-egg-uploads.png" type="image/png" length="62490" /> </item> <item> <title>PyPI hires a Safety &amp; Security Engineer</title> <author>Mike Fiedler</author> <category>hiring</category> <description>&lt;p&gt;👋 Hi there! I&#39;m Mike Fiedler (&lt;a href=&#34;https://github.com/miketheman&#34;&gt;@miketheman&lt;/a&gt;) I&#39;ve been a Python Package Index (&lt;a href=&#34;https://pypi.org/&#34;&gt;PyPI&lt;/a&gt;) contributor since early 2021, and became a maintainer in 2022. Now I&#39;m joining the &lt;a href=&#34;https://www.python.org/psf-landing/&#34;&gt;PSF&lt;/a&gt; to work on PyPI full-time as the first PyPI Safety &amp;amp; Security Engineer.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;What is that, you ask? We had &lt;a href=&#34;../2023-05-09-announcing-pypi-safety-and-security-engr-role/&#34;&gt;posted about this opening in May&lt;/a&gt;, and I&#39;m happy to be joining the team to help improve the safety and security of PyPI.&lt;/p&gt; &lt;p&gt;What does that mean, Mike? As safety and security are pretty broad topics, it boils down to working on any initiatives that we believe will increase the safety and security of the Package Index for all users - end users, package publishers, maintainers, and PyPI moderators and administrators. That&#39;s a huge audience!&lt;/p&gt; &lt;p&gt;All of us deserve to have a safe and secure experience when using PyPI, and I&#39;m excited to be able to work on this full-time.&lt;/p&gt; &lt;p&gt;Major thanks to Amazon Web Services (AWS) for their role in funding this position, and to the PSF for their continued support of the global Python community.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;em&gt;Mike Fiedler is the inaugural PyPI Safety &amp;amp; Security Engineer since 2023.&lt;/em&gt;&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2023-08-04-pypi-hires-safety-engineer/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-08-04-pypi-hires-safety-engineer/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-08-04-pypi-hires-safety-engineer.png" type="image/png" length="61666" /> </item> <item> <title>2FA Enforcement for New User Registrations</title> <author>Mike Fiedler</author> <category>2fa</category> <category>security</category> <description>&lt;h2 id=&#34;whats-changing&#34;&gt;What&#39;s changing?&lt;/h2&gt; &lt;p&gt;Starting today, &lt;strong&gt;newly registered users must enable 2FA before they can perform any management actions on PyPI&lt;/strong&gt;. This change comes after we&#39;ve also added a rule for accounts to have a verified, &lt;em&gt;primary&lt;/em&gt; email address for the same set of management actions.&lt;/p&gt; &lt;p&gt;As a reminder, PyPI has supported adding 2FA since 2019.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;This change is continuing along the path of enforcing 2FA for all users. In May of this year we &lt;a href=&#34;../2023-05-25-securing-pypi-with-2fa/&#34;&gt;announced&lt;/a&gt; that by the end of 2023 PyPI will require all users to enable Two-Factor Authentication (2FA). That post has a wealth of information on what enforcement means, and how folks can prepare for the change before end of year.&lt;/p&gt; &lt;h2 id=&#34;what-are-management-actions&#34;&gt;What are management actions?&lt;/h2&gt; &lt;p&gt;Management actions may include any of the following:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Creating/managing Projects&lt;/li&gt; &lt;li&gt;Adding/removing API Tokens&lt;/li&gt; &lt;li&gt;Uploading/removing Releases&lt;/li&gt; &lt;li&gt;Adding/removing Collaborators&lt;/li&gt; &lt;li&gt;Requesting/managing &lt;a href=&#34;../2023-04-23-introducing-pypi-organizations/&#34;&gt;Organizations&lt;/a&gt;&lt;/li&gt; &lt;li&gt;Adding/managing &lt;a href=&#34;../2023-04-20-introducing-trusted-publishers/&#34;&gt;Trusted Publishers&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;This is not an exhaustive list, but should provide a good idea of the actions we&#39;re talking about.&lt;/p&gt; &lt;h2 id=&#34;how-does-this-affect-me&#34;&gt;How does this affect me?&lt;/h2&gt; &lt;p&gt;If you only need to browse, download, and install packages from PyPI then a PyPI account isn&#39;t needed so this change doesn&#39;t affect you.&lt;/p&gt; &lt;p&gt;If you&#39;ve already enabled 2FA on your PyPI account, this change will not affect you. Thanks for doing your part to keep the Python ecosystem safe!&lt;/p&gt; &lt;p&gt;If you recently registered a new PyPI account, you are required to enable 2FA before you can perform any management actions. When attempting to perform a management action, you may see a red banner flash at the top of the page, and be redirected to the 2FA setup page for your account.&lt;/p&gt; &lt;p&gt;You will still be able to log in, browse, and download packages without 2FA. But to perform any management actions, you&#39;ll need to enable 2FA.&lt;/p&gt; &lt;h2 id=&#34;is-this-the-end&#34;&gt;Is this the end?&lt;/h2&gt; &lt;p&gt;As a reminder, we will enforce the 2FA requirement for all PyPI users at the end of 2023.&lt;/p&gt; &lt;p&gt;These changes intend to mitigate scenarios like account takeovers, where an attacker may be able to gain access to a user&#39;s account using only an email and password (either via phishing or credential stuffing). If a user&#39;s email account access is compromised, and the attacker is able to successfully request a password reset, the attacker would still need to bypass the 2FA requirement.&lt;/p&gt; &lt;p&gt;The more users using a Two Factor Authentication methods available, the safer we all are. Today PyPI offers both &lt;a href=&#34;https://en.wikipedia.org/wiki/Time-based_one-time_password&#34;&gt;Time-based One-Time Passwords (TOTP)&lt;/a&gt; and &lt;a href=&#34;https://en.wikipedia.org/wiki/WebAuthn&#34;&gt;WebAuthn&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Security is a spectrum. As long as we continue to make incremental progress, we&#39;ll improve the overall posture for &lt;strong&gt;all users&lt;/strong&gt; of PyPI.&lt;/p&gt; &lt;hr /&gt; &lt;p&gt;&lt;em&gt;Mike Fiedler is the PyPI Safety &amp;amp; Security Engineer since 2023.&lt;/em&gt;&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2023-08-08-2fa-enforcement-for-new-users/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-08-08-2fa-enforcement-for-new-users/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-08-08-2fa-enforcement-for-new-users.png" type="image/png" length="67997" /> </item> <item> <title>GitHub now scans public issues for PyPI secrets</title> <author>Mike Fiedler</author> <category>integrations</category> <category>security</category> <description>&lt;p&gt;Back in 2019 we &lt;a href=&#34;https://github.com/pypi/warehouse/issues/6051&#34;&gt;kicked off efforts&lt;/a&gt; to integrate with GitHub secret scanning. Due to the complexity in nature, the completed integration launched in 2021, with the volunteer-led effort by Joachim Jablon (&lt;a href=&#34;https://github.com/ewjoachim&#34;&gt;@ewjoachim&lt;/a&gt;) and the GitHub team.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;PyPI didn&#39;t have a blog back then, but GitHub did! Here&#39;s &lt;a href=&#34;https://github.blog/changelog/2021-03-22-the-python-package-index-is-now-a-github-secret-scanning-integrator/&#34;&gt;a link their post&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;The completed integration increased security for all PyPI users. If a user accidentally made their PyPI token public by committing it and pushing it to a GitHub public repository, GitHub would notify us and we would automatically revoke the token to prevent any misuse. This process often completes within seconds.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Cool, Mike, that was two years ago, so what?&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;GitHub &lt;a href=&#34;https://github.blog/changelog/2023-08-16-secret-scanning-detects-secrets-in-issues-for-free-public-repositories/&#34;&gt;announced yesterday&lt;/a&gt; that they will now notify users about any secrets exposed in:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;... issue&#39;s title, description, or comments, including historical revisions ...&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;This is a great enhancement to their existing secret scanning capabilities, previously only scanning &lt;strong&gt;code&lt;/strong&gt;.&lt;/p&gt; &lt;p&gt;And even better, we don&#39;t have to change anything, as we continue to receive the same notifications from our existing integration with GitHub. 🎉&lt;/p&gt; &lt;p&gt;When implementing the integration, we set up some metrics to track the number of tokens revoked. Here&#39;s a visualization showing the amount of inbound notifications from GitHub, and the number of tokens we&#39;ve revoked &lt;em&gt;(click for larger image)&lt;/em&gt;:&lt;/p&gt; &lt;figure&gt; &lt;p&gt;&lt;a href=&#34;../../assets/dd-gh-token-scanning_2023-08-16_18.12.26.png&#34;&gt;&lt;img alt=&#34;GitHub secret scanning metrics&#34; src=&#34;../../assets/dd-gh-token-scanning_2023-08-16_18.12.26.png&#34; /&gt;&lt;/a&gt; &lt;/p&gt; &lt;figcaption&gt;Bar charts of values over period of 1 year, and total counts of: received (5.87k), valid (537), processed (535)&lt;/figcaption&gt; &lt;/figure&gt; &lt;p&gt;We show 15 months of data, thanks to our infrastructure sponsor &lt;a href=&#34;https://www.datadoghq.com/&#34;&gt;Datadog&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;The large discrepancy between &#34;received&#34; and &#34;valid&#34; can be due to GitHub reporting the same token multiple times. This can happen if the token is used in multiple repositories, forks, etc.&lt;/p&gt; &lt;p&gt;The small discrepancy between &#34;valid&#34; and &#34;processed&#34; is due to that we may raise an exception when attempting to email the owner of a token that has been revoked, after which we don&#39;t mark it as &#34;processed&#34; when exception is raised. The second time the task runs, the token is now invalid, and doesn&#39;t increment the &#34;valid&#34; count.&lt;/p&gt; &lt;p&gt;Read &lt;a href=&#34;https://github.blog/changelog/2023-08-16-secret-scanning-detects-secrets-in-issues-for-free-public-repositories/&#34;&gt;GitHub&#39;s notice on The Changelog&lt;/a&gt; for more details.&lt;/p&gt; &lt;p&gt;If your organization performs this kind of secret scanning, we&#39;d love to hear from you and integrate with your tooling.&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2023-08-17-github-token-scanning-for-public-repos/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-08-17-github-token-scanning-for-public-repos/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-08-17-github-token-scanning-for-public-repos.png" type="image/png" length="72393" /> </item> <item> <title>Inbound Malware Volume Report</title> <author>Mike Fiedler</author> <category>security</category> <category>transparency</category> <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt; &lt;p&gt;The current &lt;a href=&#34;https://pypi.org/security/&#34;&gt;PyPI security reporting procedure&lt;/a&gt; directs reporters to send an email 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; with details. &lt;code&gt;security@&lt;/code&gt; was previously an email alias for &lt;code&gt;admin@&lt;/code&gt;, a Google Group that contains all current PyPI Administrators (4 people).&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;I&#39;ll refer to the &lt;code&gt;security@&lt;/code&gt; address as the &lt;strong&gt;Security Inbox&lt;/strong&gt; herein, despite it not being a traditional inbox, and what changes we&#39;ve made to it.&lt;/p&gt; &lt;p&gt;The inbound reporting workflow was roughly:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Admins receive an email to the Security Inbox&lt;/li&gt; &lt;li&gt;Any admin reads the email&lt;/li&gt; &lt;li&gt;Admin inspects the indicators of compromise (IOC), package and user history. Often using &lt;a href=&#34;https://inspector.pypi.io/&#34;&gt;inspector.pypi.io&lt;/a&gt; to investigate package contents&lt;/li&gt; &lt;li&gt;Admin takes an action (oftentimes to remove as malware)&lt;/li&gt; &lt;li&gt;Admin responds to reporter and CCs the Security Inbox for tracking&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Here&#39;s a rough sequence diagram demonstrating the notification flow:&lt;/p&gt; &lt;pre class=&#34;mermaid&#34;&gt;&lt;code&gt;sequenceDiagram participant External participant googleMail as Google Mail Routing participant googleGroup as Security Inbox&amp;lt;br/&amp;gt;(Google Group) participant pypiAdmins as PyPI Admins External-&amp;gt;&amp;gt;googleMail: sends an email report googleMail-&amp;gt;&amp;gt;googleGroup: sends an email report googleGroup-&amp;gt;&amp;gt;+pypiAdmins: sends an email report pypiAdmins--&amp;gt;&amp;gt;+External: Hi, thanks for your report... pypiAdmins--&amp;gt;&amp;gt;-googleGroup: CC response to group title Previous Inbound Flow&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;I wanted to answer a couple of questions, so as to have some data to work with:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;How many inbound malware reports do we receive? (daily/weekly/monthly)&lt;/li&gt; &lt;li&gt;How long does it take for a response from an administrator to remove the reported malware?&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;Answering these questions with the email-based system will not be 100% accurate, as there are some conditions that lead to inaccuracies, but since we&#39;re looking at large volumes of records, it&#39;s unlikely that the inaccuracies will lead to material differences in the numbers. Some examples:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;A reporter submits multiple reports on a single message&lt;/li&gt; &lt;li&gt;A reporter replies to their original email for a new report, instead of starting a new thread/conversation&lt;/li&gt; &lt;li&gt;Folks contacting admins for support may email the Security Inbox&lt;/li&gt; &lt;li&gt;Security-related issues that are not malware reports&lt;/li&gt; &lt;li&gt;Spam&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;This was largely to establish a baseline of where we are today. With this measurement, we can then observe whether changes to our process have a positive or negative impact on the volume and response times.&lt;/p&gt; &lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: This analysis is not an accurate measurement of distinct malware packages reported, as multiple researchers may report the same package, which will show up as distinct conversation threads. We could try to normalize the packages, however since the emails are unstructured, that may take more effort than is worthwhile. In any case, admins respond to duplicates, so there&#39;s still non-zero effort being done.&lt;/p&gt; &lt;p&gt;Earlier this year one of our admins posted some removals stats on Twitter (before we had a blog!):&lt;/p&gt; &lt;blockquote class=&#34;twitter-tweet&#34;&gt;&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;in 2022, the &lt;a href=&#34;https://twitter.com/pypi?ref_src=twsrc%5Etfw&#34;&gt;@pypi&lt;/a&gt; team removed &amp;gt;12,000 unique projects. each were instances of spam, typosquatting, dependency confusion, exfiltration and/or malware.&lt;br&gt;&lt;br&gt;2022: ~12K (mostly malware)&lt;br&gt;2021: ~27K (mostly dep confusion)&lt;br&gt;2020: ~500&lt;br&gt;2019: 65&lt;br&gt;2018: 137&lt;br&gt;2017: 38&lt;/p&gt;&amp;mdash; Dustin Ingram (@di_codes) &lt;a href=&#34;https://twitter.com/di_codes/status/1610781657128108033?ref_src=twsrc%5Etfw&#34;&gt;January 4, 2023&lt;/a&gt;&lt;/blockquote&gt; &lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt; &lt;h2 id=&#34;what-is-the-frequency-of-inbound-malware-reports&#34;&gt;What is the frequency of inbound malware reports?&lt;/h2&gt; &lt;p&gt;To determine this answer, I elected to use the emails themselves, since that&#39;s the feed we have of details. While this is an imperfect data source, it should be good enough to make some early assertions. We can also leverage any statistics generated thus far to assist in measuring future progress.&lt;/p&gt; &lt;p&gt;My PyPI Google Workspace account was created on 2023-03-02 and began receiving &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; emails after that. Google Groups does not surface any APIs I could find that allows an authenticated user to list/read conversations directly from the group.&lt;/p&gt; &lt;p&gt;This approach provides a cleaner signal-to-noise ratio, as I often delete the random emails that are sent to the Security Inbox if they are not useful (spam, marketing, etc). We can either accept this approach of data collection for the past few months, or pursue other methods for collecting longer-term data from older accounts/mailing lists, all subject to different data quality issues.&lt;/p&gt; &lt;p&gt;Using the data based on 1,303 email threads sent to the Security Inbox by 2023-08-14, we can produce this chart:&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Inbound Malware Reports by Date&#34; src=&#34;../../assets/2023-09-18-inbound-malware-reporting/inbound-by-date.png&#34; /&gt;&lt;/p&gt; &lt;p&gt;The same data, grouped by week number:&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Inbound Malware Reports by Week&#34; src=&#34;../../assets/2023-09-18-inbound-malware-reporting/inbound-by-week.png&#34; /&gt;&lt;/p&gt; &lt;p&gt;One observation is that post-&lt;a href=&#34;https://status.python.org/incidents/qy2t9mjjcc7g&#34;&gt;PyPI Weekend Suspension&lt;/a&gt; in May (Week 20), the overall volume drops for a while. There&#39;s no hard evidence as to why, but it&#39;s interesting that a brief disruption reduced some of the toil maintainers currently handle.&lt;/p&gt; &lt;p&gt;Form completeness, here&#39;s the monthly view:&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Inbound Malware Reports by Month&#34; src=&#34;../../assets/2023-09-18-inbound-malware-reporting/inbound-by-month.png&#34; /&gt;&lt;/p&gt; &lt;hr /&gt; &lt;h2 id=&#34;how-long-does-it-take-to-respond&#34;&gt;How long does it take to respond?&lt;/h2&gt; &lt;p&gt;Why is response time interesting? The longer a malicious package is available for end users to install, the more people and systems it may affect. This is further complicated to any package mirrors that capture the malware, and may not remove it as quickly as PyPI admins. We have received anecdotal evidence from reporters that PyPI admins are already quite fast at handling inbound reports (&lt;code&gt;#humblebrag&lt;/code&gt;), but let&#39;s see if we can get data out of the same emails.&lt;/p&gt; &lt;p&gt;Again, since the nature of email isn&#39;t 100% accurate in this case, we&#39;ll rely on calculating the duration of time (in minutes) between the first message of a thread and the last message of a thread. This doesn&#39;t account for the occasional behavior of a reporter re-using the same thread to report more packages, nor does it reflect any other back-and-forth communication between admins and reporters. As such, removing any threads that have more than 4 total messages helps remove outliers from the analysis.&lt;/p&gt; &lt;p&gt;Inbound reports come in at any time of day, and can also be automatically generated by reporters. It&#39;s common for reports to wait to be handled while we’re asleep. Weekends and holidays often have longer response times as well.&lt;/p&gt; &lt;p&gt;On occasion an inbound report may get overlooked, something we&#39;re trying to solve with a new system, more on this later.&lt;/p&gt; &lt;p&gt;A higher response time may indicate that the mostly-volunteer admins missed responding to it the first time around. For the purpose of this analysis, I&#39;ve excluded 7 total response times that exceed 14 days (20,160 minutes) to remove those outliers.&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Response Times by Date&#34; src=&#34;../../assets/2023-09-18-inbound-malware-reporting/med-response-times-by-date.png&#34; /&gt;&lt;/p&gt; &lt;p&gt;Using median values over averages helps us account for outliers at either end of the spectrum.&lt;/p&gt; &lt;p&gt;Applying a linear trend line to the collected data shows that response times are generally decreasing over time, which is a good thing.&lt;/p&gt; &lt;p&gt;Here&#39;s a distribution of response times for the data collected since March 2023:&lt;/p&gt; &lt;p&gt;&lt;img alt=&#34;Response Times Histogram&#34; src=&#34;../../assets/2023-09-18-inbound-malware-reporting/hist-resp-time-minutes.png&#34; /&gt;&lt;/p&gt; &lt;p&gt;This chart informs us that most responses to reports are completed in under ~485 minutes (~8 hours), and almost all are done within a few days of the report, with a long tail.&lt;/p&gt; &lt;p&gt;All said, that doesn&#39;t capture the full picture of response times, which is why we&#39;ve been working on a new system to help us respond faster, and produce better reports.&lt;/p&gt; &lt;h2 id=&#34;whats-changed-as-a-result&#34;&gt;What&#39;s changed as a result?&lt;/h2&gt; &lt;p&gt;Part of the PyPI Malware Reporting and Response project is to explore ways to decrease the response times even further, while reducing the toil on maintainers, and increasing visibility to reporters.&lt;/p&gt; &lt;p&gt;As a result of this analysis, one change we&#39;ve made so far is to leverage a shared inbox system &lt;a href=&#34;https://www.helpscout.com/&#34;&gt;Help Scout&lt;/a&gt; to receive inbound emails and allow us to tag, assign, and close out reports. This helps is by not missing reports, and preventing duplicate responses from admins.&lt;/p&gt; &lt;p&gt;Here&#39;s how we updated our Google Workspace flow so we can continue to receive inbound emails, as well as copy the conversations from Help Scout for long-term archival into Google Groups.&lt;/p&gt; &lt;pre class=&#34;mermaid&#34;&gt;&lt;code&gt;sequenceDiagram participant External participant googleMail as Google Mail Routing participant googleGroup as Google Group&amp;lt;br/&amp;gt;Archive participant helpScout as Security Inbox&amp;lt;br/&amp;gt;(Help Scout) participant pypiAdmins as PyPI Admins External-&amp;gt;&amp;gt;googleMail: sends an email report googleMail-&amp;gt;&amp;gt;+helpScout: sends an email report googleMail-&amp;gt;&amp;gt;googleGroup: archive email helpScout-&amp;gt;&amp;gt;+pypiAdmins: inbound notification pypiAdmins--&amp;gt;&amp;gt;-helpScout: Hi, thanks for your report... helpScout-&amp;gt;&amp;gt;googleGroup: archive via auto-bcc helpScout--&amp;gt;&amp;gt;-External: Hi, thanks for your report... title Updated Inbound Flow&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;There&#39;s no change to end users, and we hope our change keeps us on track to continue to respond to reports in a timely fashion.&lt;/p&gt; &lt;p&gt;Here&#39;s an initial look at the response times since we started using Help Scout, (2023-09-05) using their reporting. With a total of 31 conversations in the time period since we started using Help Scout, and comparing to the final two weeks of data from the previous chart (59 conversations), we can see that the response times have improved:&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Response time bucket&lt;/th&gt; &lt;th&gt;% of total&lt;/th&gt; &lt;th&gt;pre-Help Scout&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;&amp;lt; 15 min&lt;/td&gt; &lt;td&gt;40%&lt;/td&gt; &lt;td&gt;53%&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;15-30 min&lt;/td&gt; &lt;td&gt;20%&lt;/td&gt; &lt;td&gt;10%&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;30-60 min&lt;/td&gt; &lt;td&gt;20%&lt;/td&gt; &lt;td&gt;5%&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;1-2 hours&lt;/td&gt; &lt;td&gt;10%&lt;/td&gt; &lt;td&gt;7%&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;2-12 hours&lt;/td&gt; &lt;td&gt;10%&lt;/td&gt; &lt;td&gt;15%&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;12+ hours&lt;/td&gt; &lt;td&gt;0%&lt;/td&gt; &lt;td&gt;10%&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;We can now happily report that 80% all reports are responded to &lt;strong&gt;within 60 minutes of receipt&lt;/strong&gt;, with 100% are responded to within 12 hours.&lt;/p&gt; &lt;p&gt;We will continue to monitor our response times and volumes, and make adjustments as needed.&lt;/p&gt; &lt;h2 id=&#34;whats-next&#34;&gt;What&#39;s next?&lt;/h2&gt; &lt;p&gt;We&#39;re working on designing a new system to help us respond faster, based in inbound reports, and provide better outcomes. It&#39;s still very early, and we&#39;re incorporating a lot of ideas in the design based on collective experience of PyPI admins, external researchers and reporters.&lt;/p&gt; &lt;p&gt;We invite you to engage in the conversation on a more machine-readable format for reporting malware in &lt;a href=&#34;https://github.com/pypi/warehouse/issues/14503&#34;&gt;this GitHub Issue&lt;/a&gt;, and consider sending pull requests where appropriate.&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2023-09-18-inbound-malware-reporting/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-09-18-inbound-malware-reporting/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-09-18-inbound-malware-reporting.png" type="image/png" length="60018" /> </item> <item> <title>PyPI has completed its first security audit</title> <author>Dustin Ingram</author> <category>security</category> <category>transparency</category> <description>&lt;p&gt;&lt;em&gt;This is part one in a three-part series. See &lt;a href=&#34;../2023-11-14-2-security-audit-remediation-warehouse/&#34;&gt;part two here&lt;/a&gt;, and &lt;a href=&#34;../2023-11-14-3-security-audit-remediation-cabotage/&#34;&gt;part three here&lt;/a&gt;&lt;/em&gt;&lt;/p&gt; &lt;p&gt;We are proud to announce that PyPI has completed its first ever external security audit. This work was funded in partnership with the &lt;a href=&#34;https://www.opentech.fund/&#34;&gt;Open Technology Fund&lt;/a&gt; (OTF), &lt;a href=&#34;https://www.opentech.fund/results/supported-projects/pypi-improvements/&#34;&gt;a previous supporter&lt;/a&gt; of security-related improvements to PyPI.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;The Open Technology Fund selected &lt;a href=&#34;https://www.trailofbits.com/&#34;&gt;Trail of Bits&lt;/a&gt;, an industry-leading cybersecurity firm with significant open-source and Python experience, to perform the audit. Trail of Bits spent a total of 10 engineer-weeks of effort identifying issues, presenting those findings to the PyPI team, and assisting us as we remediated the findings.&lt;/p&gt; &lt;h2 id=&#34;scope&#34;&gt;Scope&lt;/h2&gt; &lt;p&gt;The audit was focused on &#34;Warehouse&#34;, the open-source codebase that powers &lt;a href=&#34;https://pypi.org&#34;&gt;https://pypi.org&lt;/a&gt;, and on &#34;cabotage&#34;, the custom open-source container orchestration framework we use to deploy Warehouse. It included code review of both codebases, prioritizing areas that accept user input, provide APIs and other public surfaces. The audit also covered the continuous integration / continuous deployment (CI/CD) configurations for both codebases.&lt;/p&gt; &lt;h2 id=&#34;findings&#34;&gt;Findings&lt;/h2&gt; &lt;p&gt;Overall, the auditors determined the Warehouse codebase &#34;was adequately tested and conformed to widely accepted best practices for secure Python and web development,&#34; and that while the cabotage codebase lacks the same level of testing, they did not identify any high severity issues in either codebase.&lt;/p&gt; &lt;h2 id=&#34;results-impact&#34;&gt;Results &amp;amp; Impact&lt;/h2&gt; &lt;p&gt;As a result of the audit, Trail of Bits detailed 29 different advisories discovered across both codebases. When evaluating severity level of each advisory, 14 were categorized as &#34;informational&#34;, 6 as &#34;low&#34;, 8 as &#34;medium&#34; and zero as &#34;high&#34;. At the time of writing, the PyPI team has remediated all advisories that posed a significant risk in both codebases where possible, and has worked with third-party teams to unblock additional remediations where necessary.&lt;/p&gt; &lt;h2 id=&#34;more-details&#34;&gt;More details&lt;/h2&gt; &lt;p&gt;In the interest of transparency, today we are publishing the &lt;a href=&#34;https://github.com/trailofbits/publications#technology-product-reviews&#34;&gt;full results of the audit&lt;/a&gt;, as prepared by Trail of Bits. You can read more about the audit from their perspective in their &lt;a href=&#34;https://blog.trailofbits.com/2023/11/14/our-audit-of-pypi/&#34;&gt;accompanying blog post&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Additionally, in two additional blog posts published today, Mike Fiedler (PyPI Security &amp;amp; Safety Engineer) goes into detail about &lt;a href=&#34;../2023-11-14-2-security-audit-remediation-warehouse/&#34;&gt;how we remediated these findings in Warehouse&lt;/a&gt; and Ee Durbin (Python Software Foundation Director of Infrastructure) &lt;a href=&#34;../2023-11-14-3-security-audit-remediation-cabotage/&#34;&gt;similarly details remediation&#39;s in cabotage&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h2&gt; &lt;p&gt;We would like to thank the Open Technology Fund for their continued support of PyPI and specifically for this significant security milestone for the Python ecosystem. We would also like to thank Trail of Bits for being a dependable, thorough and thoughtful partner throughout the process.&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2023-11-14-1-pypi-completes-first-security-audit/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-11-14-1-pypi-completes-first-security-audit/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-11-14-1-pypi-completes-first-security-audit.png" type="image/png" length="61950" /> </item> <item> <title>Security Audit Remediation: Warehouse</title> <author>Mike Fiedler</author> <category>security</category> <category>transparency</category> <description>&lt;p&gt;&lt;em&gt;This is part two in a three-part series. See &lt;a href=&#34;../2023-11-14-1-pypi-completes-first-security-audit/&#34;&gt;part one here&lt;/a&gt;, and &lt;a href=&#34;../2023-11-14-3-security-audit-remediation-cabotage/&#34;&gt;part three here&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt; &lt;p&gt;This post is a deeper dive into the remediation of the security audit findings for the Warehouse - the main codebase for &lt;a href=&#34;https://pypi.org&#34;&gt;PyPI.org&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;The audit report can be found &lt;a href=&#34;../2023-11-14-1-pypi-completes-first-security-audit/&#34;&gt;here&lt;/a&gt;. I highly recommend reading that for the fullest context first.&lt;/p&gt; &lt;!-- more --&gt; &lt;h2 id=&#34;findings&#34;&gt;Findings&lt;/h2&gt; &lt;p&gt;The audit report identified 18 findings for Warehouse, along with some code quality suggestions. This post will focus on the findings and their remediation. Some of the code quality suggestions were implemented, others deferred.&lt;/p&gt; &lt;p&gt;Here&#39;s a table of the items that are relevant to warehouse, and their status:&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;ID&lt;/th&gt; &lt;th&gt;Title&lt;/th&gt; &lt;th&gt;Severity&lt;/th&gt; &lt;th&gt;Difficulty&lt;/th&gt; &lt;th&gt;Status&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-1&lt;/td&gt; &lt;td&gt;Unsafe input handling in &#34;Combine PRs&#34; workflow&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/14528&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-2&lt;/td&gt; &lt;td&gt;Weak signatures used in AWS SNS verification&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;Undetermined&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/14387&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-4&lt;/td&gt; &lt;td&gt;Lack of rate limiting on endpoints that send email&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;Accepted&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-5&lt;/td&gt; &lt;td&gt;Account status information leak for frozen and disabled accounts&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/14449&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-6&lt;/td&gt; &lt;td&gt;Potential race conditions in search locking&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/14640&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-7&lt;/td&gt; &lt;td&gt;Use of multiple distinct URL parsers&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;Undetermined&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/14497&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-8&lt;/td&gt; &lt;td&gt;Overly permissive CSP headers on XML views&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/14452&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-9&lt;/td&gt; &lt;td&gt;Missing Permissions-Policy&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/pypi/infra/pull/160&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-10&lt;/td&gt; &lt;td&gt;Domain separation in file digests&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/14492&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-11&lt;/td&gt; &lt;td&gt;Object storage susceptible to TOC/TOU due to temporary files&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;Accepted&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-12&lt;/td&gt; &lt;td&gt;HTTP header is silently trusted if token mismatches&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/14499&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-13&lt;/td&gt; &lt;td&gt;Bleach library is deprecated&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;Undetermined&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/14526&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-14&lt;/td&gt; &lt;td&gt;Weak hashing in storage backends&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;Accepted&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-15&lt;/td&gt; &lt;td&gt;Uncaught exception with crafted README&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;Accepted&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-16&lt;/td&gt; &lt;td&gt;ReDoS via zxcvbn-python dependency&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;Accepted&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-23&lt;/td&gt; &lt;td&gt;Insecure XML processing in XMLRPC server&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/pypi/warehouse/pull/14491&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-27&lt;/td&gt; &lt;td&gt;Denial-of-service risk on tar.gz uploads&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;Accepted&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-29&lt;/td&gt; &lt;td&gt;Unescaped values in LIKE SQL queries&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;Accepted&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;&lt;em&gt;IDs are non-consecutive, as the audit report included findings for cabotage as well.&lt;/em&gt;&lt;/p&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;h2 id=&#34;details&#34;&gt;Details&lt;/h2&gt; &lt;p&gt;Now that you&#39;ve had a chance to read the original audit report, and can see that we&#39;ve remediated most of the findings, I wanted to take some time to dig into some specifics of particular findings.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-2-weak-signatures-used-in-aws-sns-verification&#34;&gt;TOB-PYPI-2: Weak signatures used in AWS SNS verification&lt;/h3&gt; &lt;p&gt;PyPI uses AWS SES to send emails to users. The SES configuration is set to use a &lt;a href=&#34;https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#mailing-list-notification&#34;&gt;Message Delivery Status&lt;/a&gt; topic, which sends a notification to an AWS SNS topic, which then sends a notification to our application.&lt;/p&gt; &lt;p&gt;This is useful for things like &#34;Accepted/Delivered&#34;, but more importantly &#34;Bounced&#34; and &#34;Complaint&#34; notifications, which change the status of user accounts. We don&#39;t want to send more emails to a known bad address, and we don&#39;t want to send emails to users who have marked us as spam.&lt;/p&gt; &lt;p&gt;Since PyPI receives a webhook from AWS SNS, it needs to verify the signature of the message.&lt;/p&gt; &lt;p&gt;Verifying inbound SNS messages has generally been left up to the user. &lt;a href=&#34;https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html&#34;&gt;The AWS SNS docs&lt;/a&gt; are clear about that.&lt;/p&gt; &lt;p&gt;We had previously implemented signature verification for version 1, which uses the SHA1 hash algorithm, as that is what existed when we implemented it.&lt;/p&gt; &lt;p&gt;As time evolved, and AWS SNS added support for SHA256, the path to upgrade was still left in the hands of the user. SNS still defaults to SHA1 (&lt;code&gt;SignatureVersion: &#39;1&#39;&lt;/code&gt;), and there&#39;s no Python SDK function to call to validate the signature for you.&lt;/p&gt; &lt;p&gt;This is &lt;a href=&#34;https://github.com/boto/boto3/issues/2508&#34;&gt;also an outstanding request from boto3 users&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;In September 2022, AWS SNS added support for SHA256 signatures, and shared the details in &lt;a href=&#34;https://aws.amazon.com/blogs/security/sign-amazon-sns-messages-with-sha256-hashing-for-http-subscriptions/&#34;&gt;this blog post&lt;/a&gt;. They also added support for verification in some of the client-side SDKs, but sadly Python is not one of them yet.&lt;/p&gt; &lt;p&gt;While we were already validating SignatureVersion 1, we took this opportunity to add support for SignatureVersion 2, update our settings, and now only accept SHA256 signatures.&lt;/p&gt; &lt;p&gt;As an &lt;a href=&#34;https://aws.amazon.com/developer/community/heroes/&#34;&gt;AWS Hero&lt;/a&gt;, I reached out to Farrah Campbell who heads up Modern Compute Community at AWS, and she quickly connected me with the AWS SNS service team for a chat. We discussed some of the challenges, as well as some ideas for the path forward.&lt;/p&gt; &lt;p&gt;I&#39;m hopeful that sometime in the future we will see two big things:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;message validation in &lt;code&gt;boto3&lt;/code&gt; for both signature versions This would enable us to remove &lt;a href=&#34;https://github.com/pypi/warehouse/blob/256d3e374ff8c2c3b29ac1b3040c88ddfb7e1d76/warehouse/utils/sns.py&#34;&gt;&lt;code&gt;MessageVerifier&lt;/code&gt; we added to warehouse&lt;/a&gt;, and benefit from any future enhancements to the validation process.&lt;/li&gt; &lt;li&gt;update AWS SNS to default to &lt;code&gt;SignatureVersion: 2&lt;/code&gt; (SHA256). This could be a breaking change for users who have not updated their settings, but would be a good step forward for security. This make take some time, as the new signature version was only added a year ago. I&#39;ll leave that up to the SNS service team.&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&#34;tob-pypi-4-lack-of-rate-limiting-on-endpoints-that-send-email&#34;&gt;TOB-PYPI-4: Lack of rate limiting on endpoints that send email&lt;/h3&gt; &lt;p&gt;We accepted this finding, as we need to send emails to unverified users as part of the account creation process, and we don&#39;t want to block that.&lt;/p&gt; &lt;p&gt;The finding details that PyPI doesn&#39;t apply blanket rate limiting, which is correct. The endpoints that send emails to &lt;strong&gt;unverified&lt;/strong&gt; addresses are protected via rate limiting.&lt;/p&gt; &lt;p&gt;PyPI has compensating controls in place to prevent abuse, such as preventing too many password reset emails from being sent to a single user.&lt;/p&gt; &lt;p&gt;Since the risk here was to the cost and reputation of the email service, we decided to accept this finding. At some future point we may revisit the rate limiting strategy for sending email.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-6-potential-race-conditions-in-search-locking&#34;&gt;TOB-PYPI-6: Potential race conditions in search locking&lt;/h3&gt; &lt;p&gt;This is another case of &#34;we implemented something that was good at the time, and as time went on a better solution became available&#34;.&lt;/p&gt; &lt;p&gt;We had written a context manager to handle locking the search index when performing updates, to prevent multiple processes from trying to update the search index at the same time. The implementation wasn&#39;t tied to the underlying Redis lock expiration, so could lead to the Redis-lock expiring, but Python believing it was still locked.&lt;/p&gt; &lt;p&gt;Here we updated our implementation to use a context manager that &lt;code&gt;redis-py&lt;/code&gt; now provides, instead of crafting our own.&lt;/p&gt; &lt;p&gt;A solid reminder to check back on your libraries and services now and then, to see if there&#39;s new features that can help you out.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-11-object-storage-susceptible-to-toctou-due-to-temporary-files&#34;&gt;TOB-PYPI-11: Object storage susceptible to TOC/TOU due to temporary files&lt;/h3&gt; &lt;p&gt;This is a complex timing attack, which requires a level of access to the system that would allow for a more direct attack. The finding itself details that if an attacker could execute this, they are more likely to do other kinds of damage.&lt;/p&gt; &lt;p&gt;The complexity of navigating between our various storage backends/client APIs does not appear to be worth the resulting defense in depth, given the required access level to exploit.&lt;/p&gt; &lt;p&gt;We have a &lt;a href=&#34;https://github.com/pypi/warehouse/pull/14568&#34;&gt;draft PR&lt;/a&gt; with a start of implementation should we decide to pursue this.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-14-weak-hashing-in-storage-backends&#34;&gt;TOB-PYPI-14: Weak hashing in storage backends&lt;/h3&gt; &lt;p&gt;This is specifically about the Backblaze B2 storage backend, one of PyPI&#39;s current object storage providers, which does not currently support SHA-256 checksums. They do support SHA-1 which is useful for detecting data corruption in transit, but is insufficient for non-colliding checksums - we have to use MD5 for that.&lt;/p&gt; &lt;p&gt;During the audit, we reached out to the Backblaze team to discuss and determined it&#39;s on their roadmap, and when they implement it, we&#39;ll update our usage accordingly.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-15-uncaught-exception-with-crafted-readme&#34;&gt;TOB-PYPI-15: Uncaught exception with crafted README&lt;/h3&gt; &lt;p&gt;This finding discovered a bug in &lt;code&gt;docutils&lt;/code&gt;, which PyPI uses via the &lt;code&gt;readme_renderer&lt;/code&gt; library to render project descriptions from reStructuredText and Markdown to HTML.&lt;/p&gt; &lt;p&gt;The bug is &lt;a href=&#34;https://sourceforge.net/p/docutils/bugs/474/&#34;&gt;tracked here&lt;/a&gt;, and has yet to see a response from the maintainers.&lt;/p&gt; &lt;p&gt;It only applies to client-side behavior when using reStructuredText for a README, so we&#39;ve accepted this finding. Additionally, any user performing &lt;code&gt;twine check&lt;/code&gt; prior to upload will surface this issue.&lt;/p&gt; &lt;p&gt;Once the bug is fixed, we&#39;ll update!&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-16-redos-via-zxcvbn-python-dependency&#34;&gt;TOB-PYPI-16: ReDoS via &lt;code&gt;zxcvbn-python&lt;/code&gt; dependency&lt;/h3&gt; &lt;p&gt;Direct from the audit:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;This finding is purely informational. We believe that it has virtually no impact, like many ReDoS vulnerabilities, due to Warehouse’s deployment architecture.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;Enough said.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-23-insecure-xml-processing-in-xmlrpc-server&#34;&gt;TOB-PYPI-23: Insecure XML processing in XMLRPC server&lt;/h3&gt; &lt;p&gt;The audit began when the Warehouse deployment was on Debian 11 &lt;code&gt;bullseye&lt;/code&gt;, and as part of normal maintenance we upgraded to &lt;a href=&#34;https://github.com/pypi/warehouse/pull/14491&#34;&gt;Debian 12 &lt;code&gt;bookworm&lt;/code&gt;&lt;/a&gt; while the audit was in progress.&lt;/p&gt; &lt;p&gt;Python XMLRPC uses &lt;a href=&#34;https://libexpat.github.io/&#34;&gt;&lt;code&gt;expat&lt;/code&gt;&lt;/a&gt; for XML parsing. The version of &lt;code&gt;expat&lt;/code&gt; in Debian &lt;code&gt;bullseye&lt;/code&gt; was &lt;code&gt;2.2.10&lt;/code&gt;, which was vulnerable to the specific attack detailed in the audit report.&lt;/p&gt; &lt;p&gt;With &lt;code&gt;bookworm&lt;/code&gt;, the version of &lt;a href=&#34;https://packages.debian.org/bookworm/libexpat1&#34;&gt;&lt;code&gt;expat&lt;/code&gt; is &lt;code&gt;2.5.0&lt;/code&gt;&lt;/a&gt;, which is not vulnerable. (Generally considered &lt;a href=&#34;https://github.com/python/cpython/pull/26945&#34;&gt;fixed as of 2.4.1.&lt;/a&gt;)&lt;/p&gt; &lt;p&gt;This was a tricky one to track down, as once the report came in I was unable to reproduce the issue locally, as I had already upgraded.&lt;/p&gt; &lt;p&gt;Using some &lt;code&gt;git bisect&lt;/code&gt; magic, I was able to track down the exact commit that fixed the issue (the &lt;code&gt;bookworm&lt;/code&gt; upgrade), and then it was a matter of figuring out which library had changed.&lt;/p&gt; &lt;p&gt;After figuring it out, I worked with the auditors to update their recommendations to reflect the upgrade. Until now, the general recommendation was to adopt &lt;code&gt;defusedxml&lt;/code&gt;, which might have proven harder as we delegate the majority of our XML parsing to &lt;code&gt;pyramid-rpc&lt;/code&gt;, which uses &lt;code&gt;xmlrpc.client&lt;/code&gt; from the standard library.&lt;/p&gt; &lt;p&gt;If you want to check your own installation, you can run the following:&lt;/p&gt; &lt;pre class=&#34;highlight&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;python -c &#34;import pyexpat; print(pyexpat.EXPAT_VERSION)&#34;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;On &lt;code&gt;bookworm&lt;/code&gt;, we get &lt;code&gt;expat_2.5.0&lt;/code&gt;, which is not affected by the vulnerability.&lt;/p&gt; &lt;p&gt;This was remediated by underlying OS update to &lt;code&gt;bookworm&lt;/code&gt;. Debian distributions pin to a specific version of libraries for the duration of that distribution version.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-27-denial-of-service-risk-on-targz-uploads&#34;&gt;TOB-PYPI-27: Denial-of-service risk on tar.gz uploads&lt;/h3&gt; &lt;p&gt;This is a tricky one, as it&#39;s a tradeoff between usability and security.&lt;/p&gt; &lt;p&gt;The audit report details a specific attack vector, where a malicious user could upload a tarball with a highly-compressed file, which would cause the server to spend a lot of time decompressing it.&lt;/p&gt; &lt;p&gt;Since we accept uploads from the general public, we have to take precautions whenever possible to prevent abuse. When it comes to ZIP files (which all &lt;code&gt;.whl&lt;/code&gt; or &#34;wheel&#34; files are), we already have a mechanism to detect decompression bombs, and reject them.&lt;/p&gt; &lt;p&gt;However, since &lt;code&gt;.tar.gz&lt;/code&gt; files do not advertise file sizes as metadata, in order to detect a decompression bomb we would have to decompress the entire file anyhow.&lt;/p&gt; &lt;p&gt;As the report notes, our deployment architecture compensates for this behavior, where we have a dedicated worker pool for handling uploads.&lt;/p&gt; &lt;p&gt;We may apply additional restrictions at the system level in the future, but for now we&#39;ve accepted this finding.&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-29-unescaped-values-in-like-sql-queries&#34;&gt;TOB-PYPI-29: Unescaped values in &lt;code&gt;LIKE SQL&lt;/code&gt; queries&lt;/h3&gt; &lt;p&gt;The risk here is that a query could &#34;walk the table&#34; and not take advantage of any indexes, leading to higher resource usage.&lt;/p&gt; &lt;p&gt;The majority of the places where we use unescaped &lt;code&gt;LIKE&lt;/code&gt; queries is in PyPI admin-only interface, where want to allow admins to search for users, packages, etc.&lt;/p&gt; &lt;p&gt;For the one place where we allow public-facing &lt;code&gt;LIKE&lt;/code&gt; queries, there are already rate limits in place to prevent abuse. The table in question is also smaller than 1M rows, so walking an un-indexed column would not be a significant resource usage, and takes a handful of extra milliseconds.&lt;/p&gt; &lt;p&gt;The potential higher resource usage would be limited to malicious internal actors, and if we can&#39;t trust each other, we&#39;ve got bigger problems to deal with.&lt;/p&gt; &lt;p&gt;We&#39;ve accepted this finding, and will continue to monitor all of relevant resources.&lt;/p&gt; &lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt; &lt;p&gt;Working with the folks at &lt;a href=&#34;https://www.trailofbits.com/&#34;&gt;Trail of Bits&lt;/a&gt; was a pleasure, and I&#39;m thankful for their thoroughness and professionalism.&lt;/p&gt; &lt;p&gt;While the audit was funded through the &lt;a href=&#34;https://opentech.fund/&#34;&gt;Open Technology Fund&lt;/a&gt;, my work on remediation would not have been as timely if not funded by &lt;a href=&#34;https://aws.amazon.com/&#34;&gt;Amazon Web Services&lt;/a&gt; to work as the &lt;a href=&#34;../2023-05-09-announcing-pypi-safety-and-security-engr-role/&#34;&gt;PyPI Safety and Security Engineer&lt;/a&gt;. I am grateful for the continued support of both organizations in making PyPI a safer place for all Python users.&lt;/p&gt; &lt;p&gt;&lt;em&gt;Mike Fiedler is the inaugural PyPI Safety &amp;amp; Security Engineer.&lt;/em&gt;&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2023-11-14-2-security-audit-remediation-warehouse/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-11-14-2-security-audit-remediation-warehouse/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-11-14-2-security-audit-remediation-warehouse.png" type="image/png" length="70101" /> </item> <item> <title>Security Audit Remediation: cabotage</title> <author>Ee Durbin</author> <category>infrastructure</category> <category>security</category> <category>transparency</category> <description>&lt;p&gt;&lt;em&gt;This is part three in a three-part series. See &lt;a href=&#34;../2023-11-14-1-pypi-completes-first-security-audit/&#34;&gt;part one here&lt;/a&gt;, and &lt;a href=&#34;../2023-11-14-2-security-audit-remediation-warehouse/&#34;&gt;part two here&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt; &lt;p&gt;This post is a deeper dive into the remediation of the security audit findings for cabotage - the &lt;a href=&#34;https://github.com/cabotage/cabotage-app&#34;&gt;codebase&lt;/a&gt; that deploys &lt;a href=&#34;https://pypi.org&#34;&gt;PyPI&lt;/a&gt; and its supporting services such as &lt;a href=&#34;https://github.com/pypi/conveyor&#34;&gt;conveyor&lt;/a&gt;, &lt;a href=&#34;https://github.com/pypi/camo&#34;&gt;camo&lt;/a&gt;, and &lt;a href=&#34;https://github.com/pypi/inspector&#34;&gt;inspector&lt;/a&gt;.&lt;/p&gt; &lt;!-- more --&gt; &lt;p&gt;Relative to the &lt;a href=&#34;https://github.com/pypi/warehouse&#34;&gt;warehouse codebase&lt;/a&gt; that &lt;em&gt;is&lt;/em&gt; &lt;a href=&#34;https://pypi.org&#34;&gt;PyPI&lt;/a&gt;, cabotage is not as widely known. The goals of cabotage are to provide a seamless and secure way of deploying arbitrary services into a &lt;a href=&#34;https://kubernetes.io&#34;&gt;Kubernetes&lt;/a&gt; cluster in a &#34;&lt;a href=&#34;https://12factor.net&#34;&gt;Twelve-Factor&lt;/a&gt;&#34; style. There are also a number of firm opinions baked into cabotage that provide end-to-end TLS, protection against recovering secrets through the web UI, and isolation between tenants inside the cluster.&lt;/p&gt; &lt;p&gt;cabotage was initially developed in 2018 as part of the &lt;a href=&#34;https://pyfound.blogspot.com/2017/11/the-psf-awarded-moss-grant-pypi.html&#34;&gt;Mozilla Open Source Support Award&lt;/a&gt; that enabled the &lt;a href=&#34;https://python.org/psf/&#34;&gt;Python Software Foundation&lt;/a&gt; (PSF) to fund a team of contracted developers and a project manager to complete the development and deployment of warehouse and sunset the &lt;a href=&#34;https://github.com/pypi/legacy&#34;&gt;original PyPI codebase&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;A primary goal of cabotage is to reduce the PSF Infrastructure&#39;s dependence on a specific provider for running PyPI, while providing self-service of configuration for project administrators and fully automated deployments. It is in-effect a &#34;Platform as a Service&#34; that deploys applications into bog-standard Kubernetes clusters, no YAML required.&lt;/p&gt; &lt;p&gt;To date, cabotage has deployed 3,901 releases to PyPI since 2018, and 7,377 releases in total across its current services &#34;fleet&#34;.&lt;/p&gt; &lt;p&gt;The audit report can be found &lt;a href=&#34;../2023-11-14-1-pypi-completes-first-security-audit/&#34;&gt;here&lt;/a&gt;. Reading that &lt;em&gt;before&lt;/em&gt; you dive in will provide the fullest context.&lt;/p&gt; &lt;h2 id=&#34;findings&#34;&gt;Findings&lt;/h2&gt; &lt;p&gt;Eleven findings resulted from the audit along with twelve code quality suggestions. This post will focus on the findings and their remediation. Some of the code quality suggestions were implemented, others deferred.&lt;/p&gt; &lt;p&gt;Here&#39;s a table of the items that are relevant to cabotage, and their status:&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;ID&lt;/th&gt; &lt;th&gt;Title&lt;/th&gt; &lt;th&gt;Severity&lt;/th&gt; &lt;th&gt;Difficulty&lt;/th&gt; &lt;th&gt;Status&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-3&lt;/td&gt; &lt;td&gt;Vulnerable dependencies in cabotage&lt;/td&gt; &lt;td&gt;Undetermined&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/cabotage/cabotage-app/compare/ad532ea0dcb7c3dd5228ee8b1312ddad9c70c6af...3e045eea677dd4af71239aad682c4b8ab3a39d3a&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-17&lt;/td&gt; &lt;td&gt;Use of shell=True in subprocesses&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/36&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-18&lt;/td&gt; &lt;td&gt;Use of HMAC with SHA1 for GitHub webhook payload validation&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/37&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-19&lt;/td&gt; &lt;td&gt;Potential container image manipulation through malicious Procfile&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/39&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-20&lt;/td&gt; &lt;td&gt;Repository confusion during image building&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/46&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-21&lt;/td&gt; &lt;td&gt;Brittle X.509 certificate rewriting&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;Undetermined&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/38&#34;&gt;Accepted&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-22&lt;/td&gt; &lt;td&gt;Unused dependencies in cabotage&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;Undetermined&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/35&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-24&lt;/td&gt; &lt;td&gt;Missing resource integrity check of third-party resources&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/40&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-25&lt;/td&gt; &lt;td&gt;Brittle secret filtering in logs&lt;/td&gt; &lt;td&gt;Medium&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/47&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-26&lt;/td&gt; &lt;td&gt;Routes missing access controls&lt;/td&gt; &lt;td&gt;Low&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/41&#34;&gt;Remediated&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;TOB-PYPI-28&lt;/td&gt; &lt;td&gt;Deployment hook susceptible to race condition due to temp files&lt;/td&gt; &lt;td&gt;Informational&lt;/td&gt; &lt;td&gt;High&lt;/td&gt; &lt;td&gt;Remediated &lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/45&#34;&gt;1&lt;/a&gt;, &lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/42&#34;&gt;2&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;&lt;em&gt;IDs are non-consecutive, as the audit report included findings for Warehouse as well.&lt;/em&gt;&lt;/p&gt; &lt;h2 id=&#34;details&#34;&gt;Details&lt;/h2&gt; &lt;h3 id=&#34;tob-pypi-3-vulnerable-dependencies-in-cabotage&#34;&gt;TOB-PYPI-3: Vulnerable dependencies in cabotage&lt;/h3&gt; &lt;p&gt;The maintenance of cabotage has been primarily driven by the need for new features or to mitigate issues raised. As a result dependency management and upgrades have often been done as a byproduct of other changes.&lt;/p&gt; &lt;p&gt;During review, there were a number of dependencies with known vulnerabilities found. Of the nine vulnerabilities noted, only &lt;a href=&#34;https://github.com/advisories/GHSA-cg8c-gc2j-2wf7&#34;&gt;GHSA-cg8c-gc2j-2wf7&lt;/a&gt; was determined impact cabotage and was remediated by migrating to the latest release of the maintained fork of flask-security, flask-security-too (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/compare/3f3e2ba7753f8e4434b562d226118a0dfe873498...3e045eea677dd4af71239aad682c4b8ab3a39d3a&#34;&gt;diff&lt;/a&gt;).&lt;/p&gt; &lt;p&gt;In order to avoid falling behind in this kind of maintenance, automated dependency management was added along with updates to all of known vulnerable dependencies (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/compare/ad532ea0dcb7c3dd5228ee8b1312ddad9c70c6af...3f3e2ba7753f8e4434b562d226118a0dfe873498&#34;&gt;diff&lt;/a&gt;).&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-17-use-of-shelltrue-in-subprocesses&#34;&gt;TOB-PYPI-17: Use of &lt;code&gt;shell=True&lt;/code&gt; in subprocesses&lt;/h3&gt; &lt;p&gt;An attack vector was identified in the way that cabotage calls out to &lt;a href=&#34;https://github.com/moby/buildkit/tree/master/cmd/buildctl&#34;&gt;&lt;code&gt;buildctl&lt;/code&gt;&lt;/a&gt; when running container builds in development mode. A specifically crafted user-input has the ability to run arbitrary shell commands on the application host.&lt;/p&gt; &lt;p&gt;Ultimately this was not determined to be exploitable in the production instance of cabotage, since the shell commands were only used when building containers in local development mode. The use of &lt;code&gt;shell=True&lt;/code&gt; was removed none-the-less as a matter of hygiene (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/36/files&#34;&gt;diff&lt;/a&gt;).&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-18-use-of-hmac-with-sha1-for-github-webhook-payload-validation&#34;&gt;TOB-PYPI-18: Use of HMAC with SHA1 for GitHub webhook payload validation&lt;/h3&gt; &lt;p&gt;Similar to the SNS verification finding in TOB-PYPI-2 for warehouse, the endpoint that received webhook payloads from GitHub for automated deployments was using SHA1 HMAC signatures to validate authenticity when SHA256 HMAC signatures were available.&lt;/p&gt; &lt;p&gt;The remediation of this finding was much more direct than the SNS finding, as GitHub began sending the SHA256 signature in the header, does not require any changes to the configuration of the webhook, and uses standard HMAC signing supported by the Python standard library (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/37/files&#34;&gt;diff&lt;/a&gt;).&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-19-potential-container-image-manipulation-through-malicious-procfile&#34;&gt;TOB-PYPI-19: Potential container image manipulation through malicious Procfile&lt;/h3&gt; &lt;p&gt;Along the same lines as TOB-PYPI-17, some user-supplied values had the ability to alter the cabotage controlled Dockerfile that specifies how release containers are built, which should not be modifiable. Through specifically crafted process names in the Procfile, a user could alter the resulting Dockerfile by injecting newlines.&lt;/p&gt; &lt;p&gt;Remediation was straightforward by adding additional validation of user supplied process names (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/39/files&#34;&gt;diff&lt;/a&gt;).&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-20-repository-confusion-during-image-building&#34;&gt;TOB-PYPI-20: Repository confusion during image building&lt;/h3&gt; &lt;p&gt;Due to a quirk in GitHub&#39;s API for fetching references, a given reference may return a concrete SHA/commit that belongs to a repository other than the one specified in the API call. In this case by providing a reference that resolves to a commit on a fork of the configured repository, a user of cabotage had the ability to intentionally (or mistakenly) configure cabotage to deploy code from a repository other than the one defined.&lt;/p&gt; &lt;p&gt;By adding additional validation inspired by &lt;a href=&#34;https://www.chainguard.dev&#34;&gt;Chainguard&lt;/a&gt;&#39;s &lt;a href=&#34;https://github.com/chainguard-dev/clank/blob/a5c8412f4e8fb128d2c0919ec02d89f086afdd24/main.go#L242-L272&#34;&gt;clank&lt;/a&gt; tool, cabotage now verifies that the resulting SHA for a given reference belongs to the configured repository (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/46/files&#34;&gt;diff&lt;/a&gt;).&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-21-brittle-x509-certificate-rewriting&#34;&gt;TOB-PYPI-21: Brittle X.509 certificate rewriting&lt;/h3&gt; &lt;p&gt;All containers built and deployed by cabotage are done so using short-lived authentication tokens for an internally deployed Docker &lt;a href=&#34;https://hub.docker.com/_/registry&#34;&gt;registry&lt;/a&gt; instance. The cabotage application itself provides this authentication and must publish a public key that the registry can use to validate tokens.&lt;/p&gt; &lt;p&gt;In order to avoid handling private-key material in the application, cabotage relies heavily on &lt;a href=&#34;https://www.vaultproject.io&#34;&gt;Hashicorp Vault&lt;/a&gt;. The transit backend for vault &lt;a href=&#34;https://github.com/hashicorp/vault/issues/3845&#34;&gt;did not support publishing the required x509 certificate&lt;/a&gt; that Docker registry required when cabotage was originally developed in 2018, so some clever use of the cryptography library was employed to create the necessary file &lt;a href=&#34;https://github.com/cabotage/cabotage-app/blob/f01b75222280699dcaa99aff3ec60e1d1d1830fc/cabotage/utils/cert_hacks.py&#34;&gt;ref&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;In the audit it was determined that this work around was brittle in the event that an attacker had the ability to alter the length of the signature, resulting in an invalid x509 certificate and broken authentication for registry clients.&lt;/p&gt; &lt;p&gt;In practice, this has not been observed in the five and a half years that it has been in production and the result of a successful attack would only lead to deployments being halted. As such, we have accepted this finding for the time being and will investigate the newly released x509 support in vault 1.15 and adopt it if able (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/issues/43&#34;&gt;issue&lt;/a&gt;).&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-22-unused-dependencies-in-cabotage&#34;&gt;TOB-PYPI-22: Unused dependencies in cabotage&lt;/h3&gt; &lt;p&gt;Similar to TOB-PYPI-3, dependency management for cabotage was lacking. This led to a handful of dependencies being installed that could be additional exposure to vulnerabilities or attacks.&lt;/p&gt; &lt;p&gt;By adopting &lt;a href=&#34;https://pypi.org/project/pip-tools/&#34;&gt;&lt;code&gt;pip-tools&lt;/code&gt;&lt;/a&gt; to compile and pin dependencies, only the projects necessary are installed (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/35/files&#34;&gt;diff&lt;/a&gt;).&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-24-missing-resource-integrity-check-of-third-party-resources&#34;&gt;TOB-PYPI-24: Missing resource integrity check of third-party resources&lt;/h3&gt; &lt;p&gt;When adding support for a new feature, third party JavaScript was added without subresource integrity information being added. This addition guards against malicious replacement of JavaScript an is good practice when loading any third party code.&lt;/p&gt; &lt;p&gt;Remediation was simple, by ensuring that all CDN loaded JavaScript had the correct value set (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/40/files&#34;&gt;diff&lt;/a&gt;).&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-25-brittle-secret-filtering-in-logs&#34;&gt;TOB-PYPI-25: Brittle secret filtering in logs&lt;/h3&gt; &lt;p&gt;There was a brief period where cabotage supported building from private GitHub repositories, which necessitated filtering build logs and removing the plaintext authentication tokens.&lt;/p&gt; &lt;p&gt;This filtering was naive, but also no longer required. Remediation was removal of the filtering code, and a comment directing a future developer to the correct way of providing such authentication for builds in the future, should building from private GitHub repositories be supported (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/47/files&#34;&gt;diff&lt;/a&gt;).&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-26-routes-missing-access-controls&#34;&gt;TOB-PYPI-26: Routes missing access controls&lt;/h3&gt; &lt;p&gt;Another vestigial piece of code that allowed for the build context necessary for container builds was identified as allowing for potentially non-public information to be leaked if a release id (UUIDv4) was guessed or surmised.&lt;/p&gt; &lt;p&gt;This route was unauthenticated as a shortcut rather than adding a new authentication method to cabotage itself.&lt;/p&gt; &lt;p&gt;This code was made defunct when cabotage began building from contexts pulled directly from GitHub and supplied via Kubernetes secrets. Remediation was again, a simple removal of the code (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/41/files&#34;&gt;diff&lt;/a&gt;).&lt;/p&gt; &lt;h3 id=&#34;tob-pypi-28-deployment-hook-susceptible-to-race-condition-due-to-temp-files&#34;&gt;TOB-PYPI-28: Deployment hook susceptible to race condition due to temp files&lt;/h3&gt; &lt;p&gt;A final vestigial piece of code was also flagged as part of the audit which was created to fetch and re-package source code from GitHub for deployments. This had the very outside potential of being exploitable if an attacker gained access to the filesystem that the cabotage app uses for temporary files.&lt;/p&gt; &lt;p&gt;This was similarly made defunct when cabotage began building from contexts pulled directly from GitHub. Remediation was a final simple removal of code (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/45/files&#34;&gt;diff-0&lt;/a&gt;), and a refactor of how temporary files are created and opened (&lt;a href=&#34;https://github.com/cabotage/cabotage-app/pull/42/files&#34;&gt;diff-1&lt;/a&gt;).&lt;/p&gt; &lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt; &lt;p&gt;In addition to the specific findings, the Trail of Bits team also made a number of &#34;Code Quality Recommendations&#34; and analyzed the overall maturity of the codebase. Those sections of the report highlight one of the two themes I see in the report regarding cabotage:&lt;/p&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;Overall the development experience and continuous integration environment for cabotage is lacking.&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;There are countless minutiae that one must consider when writing code with security in mind.&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;p&gt;In the end, no show stopping or easily exploitable security issues were found, which is a relief! Many of the most interesting security findings were only exploitable by a malicious internal actor who already had configuration permissions in cabotage, was deploying their app there in the first place, or had access to the underlying systems.&lt;/p&gt; &lt;p&gt;The takeaway I have as the sole author and maintainer of cabotage is pretty resounding, and addresses both themes from the report:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;Projects with solo maintainers do not benefit from the accountability that comes with collaborative development, are prone to deprioritizing critical improvements to developer experience and testing, and don&#39;t have the extra sets of eyes that often assist in spotting small bugs or improper handling of security sensitive software.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;So if you&#39;re interested in infrastructure and projects that make deploying software securely and reliably more straightforward, I&#39;d love to talk more. Swing by the &lt;a href=&#34;https://github.com/cabotage/cabotage-app&#34;&gt;cabotage repo&lt;/a&gt; and consider helping build the software that deploys PyPI, and will soon be deploying more and more of the &lt;a href=&#34;https://python.org/psf/&#34;&gt;Python Software Foundation&lt;/a&gt;&#39;s infrastructure as we migrate from previously gratis PaaS hosting providers.&lt;/p&gt;</description> <link>https://blog.pypi.org/posts/2023-11-14-3-security-audit-remediation-cabotage/</link> <pubDate>Wed, 18 Mar 2026 18:14:42 +0000</pubDate> <source url="https://blog.pypi.org/feed_rss_updated.xml">The Python Package Index Blog</source><guid isPermaLink="true">https://blog.pypi.org/posts/2023-11-14-3-security-audit-remediation-cabotage/</guid> <enclosure url="https://blog.pypi.org/assets/images/social/posts/2023-11-14-3-security-audit-remediation-cabotage.png" type="image/png" length="68687" /> </item> </channel> </rss>