{"version": "https://jsonfeed.org/version/1.1", "title": "The Python Package Index Blog", "home_page_url": "https://blog.pypi.org/", "feed_url": "https://blog.pypi.org/feed_json_updated.json", "description": "The official blog of the Python Package Index", "icon": "https://blog.pypi.org/assets/logo.png", "authors": [], "language": "en", "items": [{"id": "https://blog.pypi.org/posts/2026-04-16-pypi-completes-second-audit/", "url": "https://blog.pypi.org/posts/2026-04-16-pypi-completes-second-audit/", "title": "PyPI has completed its second audit", "content_html": "<p>In 2023 <a href=\"../2023-11-14-1-pypi-completes-first-security-audit/\">PyPI completed its first security audit</a>,\nand I am proud to announce that we have now completed our second external security audit.</p>\n<p>This work was funded by the <a href=\"https://www.sovereign.tech/\">Sovereign Tech Agency</a>,\na supporter of Open Source security-related improvements,\npartnering with <a href=\"https://www.trailofbits.com/\">Trail of Bits</a> to perform the audit.\nThanks to ongoing support from <a href=\"https://alpha-omega.dev/\">Alpha-Omega</a>,\nmy role at the PSF enabled me to focus on rapid remediation of the findings.</p>\n<p>This time around, there's no three-part series, as the scope was narrower,\nfocused only on PyPI's codebase and behaviors.\nRead on for a summary of issues identified, their resolutions,\nand more details about the audit process.</p>\n<!-- more -->\n\n<p>The full audit report can be found on the <a href=\"https://github.com/trailofbits/publications/blob/master/reviews/2026-04-pypi-warehouse-securityreview.pdf\">Trail of Bits publication page</a>.\nI highly recommend reading that for the fullest context first.</p>\n<h2 id=\"findings\">Findings</h2>\n<p>Here's a table of the findings, status,\nand links to the relevant pull requests where applicable:</p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">ID</th>\n<th style=\"text-align: left;\">Title</th>\n<th style=\"text-align: left;\">Severity</th>\n<th style=\"text-align: left;\">Difficulty</th>\n<th style=\"text-align: left;\">Status</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\"><a href=\"#tob-pypi26-1-oidc-jti-anti-replay-lock-expires-before-jwt-leeway-window-closes\">TOB-PYPI26-1</a></td>\n<td style=\"text-align: left;\">OIDC JTI anti-replay lock expires before JWT leeway window closes</td>\n<td style=\"text-align: left;\">Medium</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19627\">Remediated</a></td>\n</tr>\n<tr>\n<td style=\"text-align: left;\">TOB-PYPI26-2</td>\n<td style=\"text-align: left;\">OIDC token minting is vulnerable to a TOCTOU race in JTI anti-replay</td>\n<td style=\"text-align: left;\">Low</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19625\">Remediated</a></td>\n</tr>\n<tr>\n<td style=\"text-align: left;\">TOB-PYPI26-3</td>\n<td style=\"text-align: left;\">Verification badge bypass on the home page and download URLs</td>\n<td style=\"text-align: left;\">Low</td>\n<td style=\"text-align: left;\">Low</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19628\">Remediated</a></td>\n</tr>\n<tr>\n<td style=\"text-align: left;\">TOB-PYPI26-4</td>\n<td style=\"text-align: left;\">Project-level token deletion audit events silently dropped due to data structure mismatch</td>\n<td style=\"text-align: left;\">Low</td>\n<td style=\"text-align: left;\">Low</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19652\">Remediated</a></td>\n</tr>\n<tr>\n<td style=\"text-align: left;\">TOB-PYPI26-5</td>\n<td style=\"text-align: left;\">Password reset leaks privileged account status</td>\n<td style=\"text-align: left;\">Low</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19653\">Remediated</a></td>\n</tr>\n<tr>\n<td style=\"text-align: left;\"><a href=\"#tob-pypi26-6-ip-ban-bypass-via-macaroon-api-token-authentication\">TOB-PYPI26-6</a></td>\n<td style=\"text-align: left;\">IP ban bypass via macaroon API token authentication</td>\n<td style=\"text-align: left;\">Informational</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\">Accepted</td>\n</tr>\n<tr>\n<td style=\"text-align: left;\">TOB-PYPI26-7</td>\n<td style=\"text-align: left;\">Moderators can modify organization applications due to a missing write permission check</td>\n<td style=\"text-align: left;\">Low</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19619\">Remediated</a></td>\n</tr>\n<tr>\n<td style=\"text-align: left;\"><a href=\"#tob-pypi26-8-organization-members-can-invite-new-owners-due-to-a-missing-manage-permission-check\">TOB-PYPI26-8</a></td>\n<td style=\"text-align: left;\">Organization members can invite new owners due to a missing manage permission check</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\">Medium</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19610\">Remediated</a></td>\n</tr>\n<tr>\n<td style=\"text-align: left;\">TOB-PYPI26-9</td>\n<td style=\"text-align: left;\">TOTP replay prevention bypass via space normalization mismatch between validation and storage</td>\n<td style=\"text-align: left;\">Informational</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19668\">Remediated</a></td>\n</tr>\n<tr>\n<td style=\"text-align: left;\"><a href=\"#tob-pypi26-10-wheel-metadata-is-served-to-installers-without-validation-against-upload-metadata\">TOB-PYPI26-10</a></td>\n<td style=\"text-align: left;\">Wheel METADATA is served to installers without validation against upload metadata</td>\n<td style=\"text-align: left;\">Low</td>\n<td style=\"text-align: left;\">Low</td>\n<td style=\"text-align: left;\">Accepted</td>\n</tr>\n<tr>\n<td style=\"text-align: left;\">TOB-PYPI26-11</td>\n<td style=\"text-align: left;\">IDOR in API Token Deletion Allows Any Authenticated User to Delete Other Users' Macaroons</td>\n<td style=\"text-align: left;\">Low</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19669\">Remediated</a></td>\n</tr>\n<tr>\n<td style=\"text-align: left;\">TOB-PYPI26-12</td>\n<td style=\"text-align: left;\">GitHub OIDC publisher lookup lacks issuer URL isolation for custom GHES issuers</td>\n<td style=\"text-align: left;\">Informational</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19661\">Remediated</a> <a href=\"https://github.com/pypi/warehouse/pull/19661\">1</a>, <a href=\"https://github.com/pypi/warehouse/pull/19718\">2</a></td>\n</tr>\n<tr>\n<td style=\"text-align: left;\"><a href=\"#tob-pypi26-13-organization-scoped-project-associations-persist-after-project-transfer-or-removal\">TOB-PYPI26-13</a></td>\n<td style=\"text-align: left;\">Organization-scoped project associations persist after project transfer or removal</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19749\">Remediated</a></td>\n</tr>\n<tr>\n<td style=\"text-align: left;\">TOB-PYPI26-14</td>\n<td style=\"text-align: left;\">Admin flag changes lack audit logging</td>\n<td style=\"text-align: left;\">Informational</td>\n<td style=\"text-align: left;\">High</td>\n<td style=\"text-align: left;\"><a href=\"https://github.com/pypi/warehouse/pull/19751\">Remediated</a></td>\n</tr>\n</tbody>\n</table>\n<p>Of the 14 findings, I used a combination of Severity and Difficulty\nto determine which ones to work on first, and which ones to accept for now.</p>\n<p>There were <strong>no Critical</strong> severity findings, 2 High, 1 Medium, 7 Low,\nand 3 Informational severity findings.</p>\n<p>All but 2 findings have been remediated, and the remaining 2 are accepted for now.\nMore details on the accepted findings below, but in general these were accepted\nbecause they require significant effort to remediate, and the risk they pose is relatively low.</p>\n<p>To reiterate, the <a href=\"https://github.com/trailofbits/publications/blob/master/reviews/2026-04-pypi-warehouse-securityreview.pdf\">published report PDF</a> goes into deeper detail about each finding,\nso I recommend reading that for the fullest context first.</p>\n<h2 id=\"details\">Details</h2>\n<p>For some of the Remediated entries and all the Accepted ones, I'll go into more detail below.</p>\n<h3 id=\"tob-pypi26-1-oidc-jti-anti-replay-lock-expires-before-jwt-leeway-window-closes\">TOB-PYPI26-1: OIDC JTI anti-replay lock expires before JWT leeway window closes</h3>\n<p>PyPI's <a href=\"https://docs.pypi.org/trusted-publishers/\">Trusted Publishing</a> flow\nuses OIDC JWTs issued by CI providers to mint short-lived upload tokens.\nEach JWT contains a <code>jti</code> (JWT Token Identifier) claim that should be single-use.\nTo enforce this, we store each <code>jti</code> in cache (Redis) with an expiration of <code>exp + 5</code> seconds,\nand check whether it already exists before accepting a new token.</p>\n<p>The problem: <code>PyJWT</code> is configured with <code>leeway=30</code>,\nmeaning it accepts tokens up to 30 seconds past their <code>exp</code> claim.\nThis created a 25-second window (from <code>exp + 5</code> to <code>exp + 30</code>)\nwhere the cache key had already been evicted, but the JWT still passed signature verification.\nDuring that window, a replayed token would pass both the signature check and the <code>jti</code> uniqueness check.</p>\n<p>The fix was straightforward --\nalign the cache TTL to outlive the full leeway window\nby <a href=\"https://github.com/pypi/warehouse/pull/19627/changes#diff-b6993869ec628f626827f05a87938a08f1ced03e52b68a49000100910d9d46f2R288\">setting the expiration to <code>exp + leeway + margin</code></a>.\nI also took the opportunity to centralize these time-window constants\nso they're derived from a shared configuration,\npreventing future drift when one value is updated without the other.</p>\n<h3 id=\"tob-pypi26-6-ip-ban-bypass-via-macaroon-api-token-authentication\">TOB-PYPI26-6: IP ban bypass via macaroon API token authentication</h3>\n<p><strong>Accepted for now.</strong></p>\n<p>PyPI administrators can ban IP addresses through the admin dashboard.\nThe session authentication policy enforces this by checking the IP against the ban list\nbefore returning an identity.\nHowever, the macaroon (API token) authentication policy doesn't perform this same check.\nThis means a user with a valid API token could continue uploading packages\nfrom a banned IP address.</p>\n<p>I've accepted this finding for now. IP bans are a relatively blunt tool that we use sparingly,\n<a href=\"https://github.com/pypi/warehouse/pull/19213\">introduced late last year</a>\nto mitigate a specific wave of abuse.\nThe practical risk here is low - if we've identified a malicious actor,\nwe have other mechanisms to disable their account entirely.\nThat said, it's a gap worth closing, and we'll likely address it as part of broader work\non making security controls consistent across all authentication methods.</p>\n<h3 id=\"tob-pypi26-8-organization-members-can-invite-new-owners-due-to-a-missing-manage-permission-check\">TOB-PYPI26-8: Organization members can invite new owners due to a missing manage permission check</h3>\n<p>This was the highest-severity finding in the audit, and one I prioritized immediately.</p>\n<p>The <code>manage_organization_roles</code> view handled both <code>GET</code> (viewing the people page)\nand <code>POST</code> (sending invitations) under a single <code>@view_config</code> decorator\nthat only required <code>OrganizationsRead</code> permission.\nThis meant any organization member could send invitations with <em>any</em> role -\nincluding Owner - to any PyPI user.</p>\n<p>The irony is that we already had the correct pattern elsewhere in the codebase.\nViews like <code>resend_organization_invitation</code> and <code>change_organization_role</code>\ncorrectly use separate <code>@view_config</code> decorators for <code>GET</code> and <code>POST</code>\nwith distinct permission requirements.\nThis one was simply missed.</p>\n<p>The <a href=\"https://github.com/pypi/warehouse/pull/19610\">fix was to split the view configuration</a>:\n<code>GET</code> requires <code>OrganizationsRead</code>, <code>POST</code> requires <code>OrganizationsManage</code>.\nAs part of the audit, Trail of Bits also developed a custom CodeQL query\nto detect this class of issue - views that handle state-changing <code>POST</code> requests\nunder a read-only permission check.\nI'll integrate that into our CI to catch this pattern going forward.</p>\n<h3 id=\"tob-pypi26-10-wheel-metadata-is-served-to-installers-without-validation-against-upload-metadata\">TOB-PYPI26-10: Wheel METADATA is served to installers without validation against upload metadata</h3>\n<p><strong>Accepted for now.</strong></p>\n<p>This is a nuanced one. When a wheel is uploaded to PyPI,\nwe store two independent sources of metadata:\nthe form-declared metadata from the upload request (which populates the database and the JSON API),\nand the embedded <code>.dist-info/METADATA</code> file extracted from the wheel itself\n(which is served via <a href=\"https://peps.python.org/pep-0658/\">PEP 658</a> to <code>pip</code> for dependency resolution).</p>\n<p>These two sources are never compared.\nIn theory, an attacker could embed hidden dependencies in the wheel's <code>METADATA</code>\nthat <code>pip</code> would install, but that security tools querying the JSON API would never see.</p>\n<p>We've accepted this for now because the fix is non-trivial.\nProperly validating embedded metadata against upload metadata\ntouches a core part of how we handle uploads,\nand requires careful consideration of edge cases across the ecosystem.\nThis is something we want to get right rather than rush,\nand <a href=\"https://github.com/pypi/warehouse/issues/8090\">involves a fair amount of database changes, including data backfills</a>.</p>\n<h3 id=\"tob-pypi26-13-organization-scoped-project-associations-persist-after-project-transfer-or-removal\">TOB-PYPI26-13: Organization-scoped project associations persist after project transfer or removal</h3>\n<p>This was the other High-severity finding, and a subtle one.</p>\n<p>When a project is transferred between organizations,\nthe <code>OrganizationProject</code> junction record is correctly deleted and recreated.\nHowever, the <code>TeamProjectRole</code> records - which grant a team's members\naccess to specific projects - were <em>not</em> cleaned up during the transfer.</p>\n<p>This meant that if LexCorp Organization had a \"release-engineers\" team\nwith Owner-level access to a project,\nand that project was transferred to Organization OsCorp,\nthe LexCorp team's members would silently retain full access to the project.\nWorse, the receiving organization had no visibility into these stale associations -\nteam-granted permissions are resolved at ACL evaluation time\nand don't appear as individual collaborator entries in the UI.</p>\n<p>The fix in <a href=\"https://github.com/pypi/warehouse/pull/19749\">pypi/warehouse#19749</a>\nensures that <code>TeamProjectRole</code> records belonging to the departing organization\nare cleaned up when a project is transferred.\nAuditing database records proved that this has not happened in the past,\nso I am confident there have been no such transfers with dangling permissions.\nI also added defensive filters in the project's ACL computation\nto verify that a team's organization matches the project's current organization\nbefore granting permissions,\nso stale records can't grant access regardless of how they're orphaned.</p>\n<h2 id=\"summary\">Summary</h2>\n<p>Working with <a href=\"https://www.trailofbits.com/\">Trail of Bits</a> was again a pleasure.\nThe team were thorough, communicative, and clearly understood\nthe nuances of a system like PyPI - where the threat model spans everything\nfrom CI/CD token replay to metadata integrity for millions of downstream users.</p>\n<p>Beyond the 14 findings, the audit also produced proposal reviews\nfor features I'm considering (per-org Trusted Publishers, TOTP hardening, and more),\nas well as custom CodeQL queries to integrate into our CI/CD pipeline.</p>\n<p>This audit was funded in partnership with\nthe <a href=\"https://www.sovereign.tech/\">Sovereign Tech Agency</a>,\nwhich continues to support security improvements across the Open Source ecosystem.</p>\n<p>My work at the Python Software Foundation is supported by <a href=\"https://alpha-omega.dev/\">Alpha-Omega</a>.</p>", "image": "https://blog.pypi.org/assets/images/social/posts/2026-04-16-pypi-completes-second-audit.png", "date_modified": "2026-04-10T19:38:00+00:00", "date_published": "2026-04-16T19:00:00+00:00", "authors": [{"name": "Mike Fiedler"}], "tags": ["security", "transparency"]}, {"id": "https://blog.pypi.org/posts/2026-04-02-incident-report-litellm-telnyx-supply-chain-attack/", "url": "https://blog.pypi.org/posts/2026-04-02-incident-report-litellm-telnyx-supply-chain-attack/", "title": "Incident Report: LiteLLM/Telnyx supply-chain attacks, with guidance", "content_html": "<p>This post will drill deeper into two recent supply chain exploits, targeting users of popular PyPI packages - <code>litellm</code> &amp; <code>telnyx</code>.\nWe also provide Python developers and maintainers with guidance on what they can do to prepare\nand protect themselves from future incidents.</p>\n<!-- more -->\n\n<h2 id=\"what-happened-with-litellm-and-telnyx\">What happened with LiteLLM and telnyx?</h2>\n<p>After an API token exposure from an <a href=\"https://www.aquasec.com/blog/trivy-supply-chain-attack-what-you-need-to-know/\">exploited Trivy dependency</a>\nreleases of the packages <a href=\"https://pypi.org/project/litellm/\"><code>litellm</code></a> and <a href=\"https://pypi.org/project/telnyx/\"><code>telnyx</code></a>\nwere published to PyPI containing credential harvesting malware.</p>\n<p>The malware ran on install, harvesting sensitive credentials and files, and exfiltrating to a remote API. More details published in <a href=\"https://osv.dev/vulnerability/PYSEC-2026-2\">the advisory for LiteLLM (PYSEC-2026-2)</a>, the <a href=\"https://docs.litellm.ai/blog/security-update-march-2026\">LiteLLM blog post</a> about the incident,\n<a href=\"https://osv.dev/vulnerability/PYSEC-2026-3\">the advisory for telnyx (PYSEC-2026-3)</a> and the <a href=\"https://telnyx.com/resources/telnyx-python-sdk-supply-chain-security-notice-march-2026\">telnyx notice</a>.</p>\n<p>After contacting the <code>litellm</code> and <code>telnyx</code> maintainers, Mike and Seth collaborated with each team on steps forward, including token rotation, release removals, and recommendations around further security practices like using <a href=\"https://docs.pypi.org/trusted-publishers/\">Trusted Publishers</a> which both projects have since adopted.</p>\n<h2 id=\"why-is-this-malware-different\">Why is this malware different?</h2>\n<p>This class of malware is different from most malware published to PyPI, which are mostly published as <strong>new packages</strong>, either as typosquats or with a plan to share the package with others and hope they install it. This malware is \"injected\" into open source packages that are <strong>already in widespread use</strong>. The malware injection occurs one of two ways:</p>\n<ul>\n<li>Targeting open source projects with insecure repositories, release workflows, or authentication</li>\n<li>Targeting developers installing the latest versions of open source projects and exfiltrating API tokens and keys</li>\n</ul>\n<p>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. </p>\n<h2 id=\"what-is-pypi-doing-to-mitigate-malware\">What is PyPI doing to mitigate malware?</h2>\n<p>With daily volume of <strong>~700-800 new projects</strong> created daily on PyPI, this poses a scaling challenge.\nPyPI partners with security researchers from our community who regularly scan and report malware through elevated channels to facilitate quicker remediation times.</p>\n<p>Below see the timeline of events.</p>\n<p><img alt=\"LiteLLM download timeline with reports and quarantine\" src=\"../../assets/2026-04-02-incident-report-litellm-telnyx-supply-chain-attack/litellm_downloads_timeline.png\" /></p>\n<p>During the window of attack, the exploited versions of <code>litellm</code> were downloaded over 119k times.</p>\n<p>PyPI received 13 inbound reports from concerned users, leveraging the <a href=\"../2024-03-06-malware-reporting-evolved/\">\"Report project as malware\" feature</a>, added back in 2024, accelerating the review/action.</p>\n<ul>\n<li>From upload to first report: 1h 19m</li>\n<li>First report to quarantine: 1h 12m</li>\n<li>From upload to quarantine (total exposure time): <strong>2h 32m</strong></li>\n</ul>\n<p>LiteLLM is typically installed <a href=\"https://pepy.tech/projects/litellm?timeRange=threeMonths&amp;category=version&amp;includeCIDownloads=true&amp;granularity=weekly&amp;viewType=line&amp;versions=*\">~15-20 million times per week</a>. Averaging this out to an \"installs per minute\" rate nets a value between <strong>~1700 installs per minute</strong>.\nThis means between <strong>~40-50% of all installs</strong> of LiteLLM were unpinned and fetching the latest version on each install invocation.</p>\n<p>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 \"<a href=\"#dependency-cooldowns\">dependency cooldowns</a>\"</p>\n<p><img alt=\"Telnyx download timeline with reports and quarantine\" src=\"../../assets/2026-04-02-incident-report-litellm-telnyx-supply-chain-attack/telnyx_downloads_timeline.png\" /></p>\n<p>PyPI's remediation response to <code>telnyx</code> was autonomously taken thanks to our pool of trusted reporters.\nThese reporters add weight to any given report, triggering an automated quarantine feature.\nSubscribe to the PyPI blog for a future update about our automatic quarantining system.</p>\n<ul>\n<li>From upload to first report: 1h 45m</li>\n<li>First report to quarantine: 1h 57m</li>\n<li>Form upload to quarantine (total exposure time): 3h 42m</li>\n</ul>\n<p>Below are a few methods to make your usage of Python packages from PyPI more secure and to avoid installing malware.</p>\n<h2 id=\"protecting-yourself-as-a-developer\">Protecting yourself as a developer</h2>\n<h3 id=\"dependency-cooldowns\">Dependency Cooldowns</h3>\n<p>One method to avoid \"drinking from the firehose\" and allow time for malware to be detected and remediated is using \"Dependency Cooldowns\".\nDependency cooldowns is a strategy for package installers (like <code>pip</code>, <code>uv</code>, 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.</p>\n<p>Dependency cooldowns work best when they are configured \"globally\" on a developer machine and then passively protect developers from compromises on every invocation of pip or uv.\nSetting a relative value like \"3 days\" (\"<code>P3D</code>\" per RFC 3339) means packages that are newer than 3 days will not be installed.</p>\n<p>uv already supports <a href=\"https://docs.astral.sh/uv/concepts/resolution/#dependency-cooldowns\">setting relative dependency cooldown</a> via <code>--exclude-newer</code>. You can <a href=\"https://docs.astral.sh/uv/concepts/configuration-files/\">configure the option globally</a> (in <code>~/.config/uv/uv.toml</code>) or per-project in your <code>pyproject.toml</code>:</p>\n<pre class=\"highlight\"><code class=\"language-toml\">[tool.uv]\nexclude-newer = \"P3D\"  # \"3 days\" in RFC 3339 format</code></pre>\n<p>Relative dependency cooldowns are coming soon to pip v26.1 which <a href=\"https://pip.pypa.io/en/stable/development/release-process/\">should be available in April</a> of this year. Once they are available you can set the option in <a href=\"https://pip.pypa.io/en/stable/topics/configuration/\">your <code>pip.conf</code> file</a> (<code>~/.config/pip/pip.conf</code>):</p>\n<pre class=\"highlight\"><code class=\"language-ini\">[install]\nuploaded-prior-to = P3D</code></pre>\n<p><a href=\"https://ichard26.github.io/blog/2026/01/whats-new-in-pip-26.0/#excluding-distributions-by-upload-time\">Starting in pip v26.0</a> you can set absolute dependency cooldowns with pip from the command-line, likely paired with another tool like <code>date</code> to calculate an absolute date from a relative offset like \"<code>3 days</code>\":</p>\n<pre class=\"highlight\"><code class=\"language-shell\">python -m pip install \\\n  --uploaded-prior-to=$(date -d '-3days' -Idate) \\\n  simplepackage</code></pre>\n<p>Applying dependency cooldowns everywhere isn't a silver bullet, though!\nThere are certain situations where you <em>do</em> want the latest version of a package as soon as possible, like when applying patches for vulnerabilities.\nDependency cooldowns should be paired with a vulnerability scanning strategy so security updates for your application's dependencies aren't waiting to be deployed. For example, Dependabot and Renovate both bypass dependency cooldowns by default for security updates.</p>\n<p>You can manually bypass a dependency cooldown in pip and uv by setting a value of \"the current day\" to get the actual latest release:</p>\n<pre class=\"highlight\"><code class=\"language-shell\">python -m pip install \\\n  --uploaded-prior-to=P0D \\\n  simplepackage==26.3.31</code></pre>\n<h3 id=\"locking-dependencies\">Locking Dependencies</h3>\n<p>Installing a package from PyPI without a \"lock\" means that it's possible to receive new code <em>every time you run pip install</em>.\nThis leaves the door open for a compromise of a package to immediately get installed and execute malware on the installing systems.</p>\n<p>If you're a developer of an application using PyPI packages you should be using lock files both for security and reproducibility of your application.\nSome examples of tools which produce lock files for applications are:</p>\n<ul>\n<li><code>uv lock</code></li>\n<li><code>pip-compile --generate-hashes</code></li>\n<li><code>pipenv</code></li>\n</ul>\n<p>Note that <code>pip freeze</code> doesn't create a lock file: a lock file must include checksums / hashes of the package archives to be secure and reproducible.\n<code>pip freeze</code> only records packages and their versions. pip is working on experimental support for the <code>pylock.toml</code> standard through the\n<a href=\"https://pip.pypa.io/en/stable/cli/pip_lock\"><code>pip lock</code> sub-command</a>.</p>\n<h2 id=\"protecting-your-project-as-an-open-source-maintainer\">Protecting your project as an open source maintainer</h2>\n<p>If you are a maintainer of an open source project on PyPI, you can do your part to protect your users from compromises.\nThere are three approaches we recommend: </p>\n<ol>\n<li>securing your release workflows</li>\n<li>using Trusted Publishers</li>\n<li>adding 2FA to all accounts associated with open source development</li>\n</ol>\n<h3 id=\"securing-release-workflows\">Securing release workflows</h3>\n<p>If you'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:</p>\n<ul>\n<li><strong>Avoid insecure triggers.</strong> 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 <code>pull_request_target</code> from GitHub Actions in particular is difficult to use securely and should be avoided.</li>\n<li><strong>Sanitize parameters and inputs.</strong> 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.</li>\n<li><strong>Avoid mutable references.</strong> 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.</li>\n<li><strong>Use reviewable deployments.</strong> Trusted Publishers for GitHub supports \"GitHub Environments\" 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.</li>\n</ul>\n<p>If you are using GitHub Actions as your continuous deployment provider, we highly recommend the tool \"<a href=\"https://github.com/zizmorcore/zizmor/\">Zizmor</a>\" for detecting and fixing insecure workflows.</p>\n<h3 id=\"trusted-publishers-over-api-tokens\">Trusted Publishers over API tokens</h3>\n<p>If the platform you use to publish to PyPI supports <a href=\"https://docs.pypi.org/trusted-publishers/\">Trusted Publishers</a> (GitHub, GitLab, Google Cloud Build, ActiveState) then you should use Trusted Publishers instead of API tokens.</p>\n<p>PyPI API tokens are \"long-lived\", meaning if they are exfiltrated by an attacker that attacker can use the token at a much later date even if you don't detect the initial compromise. Trusted Publishers comparatively uses short-lived tokens, meaning they need to be used immediately and don't require manual \"rotating\" in the case of compromise.</p>\n<p>Trusted Publishers also provides a valuable signal to downstream users through <a href=\"https://docs.pypi.org/attestations/\">Digital Attestations</a>. This means users can detect when a release <em>hasn't</em> been published using the typical release workflow, likely drawing more scrutiny.</p>\n<h3 id=\"adding-2fa-to-open-source-development-accounts\">Adding 2FA to open source development accounts</h3>\n<p>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.\nThink about accounts like version control / software forges\n(GitHub, GitLab, Codeberg, Forgejo) and your email provider.\n<a href=\"../2024-01-01-2fa-enforced/\">PyPI has required 2FA to be enabled to publish packages</a> since the beginning of 2024,\nbut enabling phishing-resistant 2FA like a hardware key can protect you further.</p>\n<h2 id=\"how-can-you-support-this-kind-of-work\">How can you support this kind of work?</h2>\n<p>Security work isn'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 <a href=\"https://www.python.org/sponsors/application/\">PSF's sponsorship program</a>, <a href=\"https://www.python.org/psf/donations/\">donate directly</a>, or contact our team at <a href=\"mailto:sponsors@python.org\">sponsors@python.org</a>!</p>\n<p>Mike Fiedler and Seth Larson's roles as <a href=\"https://www.python.org/psf/developersinresidence/\">PyPI Safety &amp; Security Engineer and Security Developer-in-Residence</a> at the Python Software Foundation are supported by <a href=\"https://alpha-omega.dev\">Alpha Omega</a>.</p>", "image": "https://blog.pypi.org/assets/images/social/posts/2026-04-02-incident-report-litellm-telnyx-supply-chain-attack.png", "date_modified": "2026-04-02T15:04:45+00:00", "date_published": "2026-04-02T06:09:00+00:00", "authors": [{"name": "Seth Larson"}, {"name": "Mike Fiedler"}], "tags": ["security", "transparency"]}, {"id": "https://blog.pypi.org/posts/2023-03-21-welcome-to-the-pypi-blog/", "url": "https://blog.pypi.org/posts/2023-03-21-welcome-to-the-pypi-blog/", "title": "Welcome to the PyPI Blog", "content_html": "<p>Today, we're excited to launch <a href=\"https://blog.pypi.org\">blog.pypi.org</a>,\nthe official blog of the Python Package Index.</p>\n<p>One of the most common refrains I hear from Python community members,\nirrespective of if they have been around for days or years,\nis <em>\"I didn't realize that PyPI...\"</em>. Followed by something along the lines of:</p>\n<!-- more -->\n\n<ul>\n<li>Could <strong>do</strong> that</li>\n<li>Is operated with <strong>so</strong> few resources<sup id=\"fnref:1\"><a class=\"footnote-ref\" href=\"#fn:1\">1</a></sup></li>\n<li>Only had <strong>3</strong> administrators<sup id=\"fnref:2\"><a class=\"footnote-ref\" href=\"#fn:2\">2</a></sup></li>\n<li>Needs <strong>more</strong> help</li>\n</ul>\n<p>That is certainly in part due to the fact that the people<sup id=\"fnref:3\"><a class=\"footnote-ref\" href=\"#fn:3\">3</a></sup> who\nmake PyPI \"happen\" are <em>humble</em>, and do so with the limited time they\nhave to commit to the project among their other responsibilities.\nBut its also because writing is hard. Harder still when there is\nnot a convenient tool at hand.\nWe're hoping that a modern and extensible stack<sup id=\"fnref:4\"><a class=\"footnote-ref\" href=\"#fn:4\">4</a></sup> for this blog\nwill make writing about and sharing our work more frictionless.</p>\n<p>The PyPI team will use this space to communicate with PyPI users,\nprovide announcements about new features and updates,\ninteresting technology, as well as share information and\ncontext around PyPI and related efforts of the\n<a href=\"https://www.python.org/psf-landing/\">Python Software Foundation</a>.</p>\n<p>Stay tuned for more, we're very glad you're here! \ud83d\udc9c</p>\n<hr />\n<p><em>Ee Durbin is the Director of Infrastructure at\nthe Python Software Foundation.\nThey have been contributing to keeping PyPI online, available, and\nsecure since 2013.</em></p>\n<div class=\"footnote\">\n<hr />\n<ol>\n<li id=\"fn:1\">\n<p>We're thrifty and picky about what we run, but\n<a href=\"https://www.fastly.com/fast-forward\">Fastly</a> is the single most\nimportant factor allowing us to get by with comparatively few\nresources for our backends.&#160;<a class=\"footnote-backref\" href=\"#fnref:1\" title=\"Jump back to footnote 1 in the text\">&#8617;</a></p>\n</li>\n<li id=\"fn:2\">\n<p>That was until we welcomed Mike Fiedler as an admin in 2023.&#160;<a class=\"footnote-backref\" href=\"#fnref:2\" title=\"Jump back to footnote 2 in the text\">&#8617;</a></p>\n</li>\n<li id=\"fn:3\">\n<p>The PyPI Administrator team is\n<a href=\"https://github.com/dstufft\">Donald Stufft</a>,\n<a href=\"https://github.com/di\">Dustin Ingram</a>,\n<a href=\"https://github.com/miketheman\">Mike Fiedler</a>,\nand <a href=\"https://github.com/ewdurbin\">Ee Durbin</a>.\nThe PyPI Moderator team is\n<a href=\"https://github.com/yeraydiazdiaz\">Yeray D\u00edaz</a>,\n<a href=\"https://github.com/cmaureir\">Cristi\u00e1n Maureira-Fredes</a>,\n<a href=\"https://github.com/ewjoachim\">Joachim Jablon</a>,\n<a href=\"https://github.com/jamadden\">Jason Madden</a>,\n<a href=\"https://github.com/pradyunsg\">Pradyun Gedam</a>,\nand <a href=\"https://github.com/theacodes\">Stargirl Flowers</a>.&#160;<a class=\"footnote-backref\" href=\"#fnref:3\" title=\"Jump back to footnote 3 in the text\">&#8617;</a></p>\n</li>\n<li id=\"fn:4\">\n<p>Shout-out: This blog is hosted on\n<a href=\"https://readthedocs.org/\">Read the Docs</a>,\nusing the <a href=\"https://pypi.org/project/mkdocs/\">MkDocs</a>\nstatic site generator,\nand the <a href=\"https://pypi.org/project/mkdocs-material/\">Material for MkDocs</a> theme.\nMany thanks to the maintainers of those projects for your\ncontributions,\nwe're very excited to have our blog contents live side-by-side\nwith our other documentation and code in\n<a href=\"https://github.com/pypi/warehouse\">pypi/warehouse</a>!&#160;<a class=\"footnote-backref\" href=\"#fnref:4\" title=\"Jump back to footnote 4 in the text\">&#8617;</a></p>\n</li>\n</ol>\n</div>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-03-21-welcome-to-the-pypi-blog.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-03-21T06:09:00+00:00", "authors": [{"name": "Ee Durbin"}], "tags": ["blogs-about-blogs", "meta", "welcome"]}, {"id": "https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/", "url": "https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/", "title": "Introducing 'Trusted Publishers'", "content_html": "<p>Starting today, PyPI package maintainers can adopt a new, more secure\npublishing method that does not require long-lived passwords or API tokens to\nbe shared with external systems.</p>\n<!-- more -->\n\n<h3 id=\"about-trusted-publishing\">About trusted publishing</h3>\n<p>\"Trusted publishing\" is our term for using the <a href=\"https://openid.net/connect/\">OpenID Connect (OIDC)</a> standard\nto exchange short-lived identity tokens between a trusted third-party service\nand PyPI. This method can be used in automated environments and eliminates the\nneed to use username/password combinations or manually generated API tokens to\nauthenticate with PyPI when publishing.</p>\n<p>Instead, PyPI maintainers can configure PyPI to trust an identity provided by a\ngiven OpenID Connect Identity Provider (IdP). This allows allows PyPI to verify\nand delegate trust to that identity, which is then authorized to request\nshort-lived, tightly-scoped API tokens from PyPI. These API tokens never need\nto be stored or shared, rotate automatically by expiring quickly, and provide a\nverifiable link between a published package and its source.</p>\n<h3 id=\"using-trusted-publishing-with-github-actions\">Using trusted publishing with GitHub Actions</h3>\n<p>PyPI currently supports trusted publishing with GitHub Actions, using <a href=\"https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect\">their\nsupport for OpenID Connect</a>.</p>\n<p>After configuring PyPI to trust a given GitHub repository and workflow, users\nof the PyPA's <a href=\"https://github.com/marketplace/actions/pypi-publish\">'pypi-publish' GitHub Action</a> can adopt trusted publishing by\nremoving the <code>username</code> and <code>password</code> fields from their workflow\nconfiguration, and adding permissions to generate an identity token:</p>\n<pre class=\"highlight\"><code class=\"language-diff\">jobs:\n   pypi-publish:\n     name: upload release to PyPI\n     runs-on: ubuntu-latest\n+    permissions:\n+      # IMPORTANT: this permission is mandatory for trusted publishing\n+      id-token: write\n     steps:\n       # retrieve your distributions here\n\n       - name: Publish package distributions to PyPI\n         uses: pypa/gh-action-pypi-publish@release/v1\n-        with:\n-          username: __token__\n-          password: ${{ secrets.PYPI_TOKEN }}</code></pre>\n<p>Using the PyPA's GitHub action is strongly recommended, but not required. More\ndetails on how to manually exchange tokens are available <a href=\"https://docs.pypi.org/trusted-publishers/using-a-publisher/#the-manual-way\">in our\ndocumentation</a>.</p>\n<h3 id=\"additional-security-hardening-is-available\">Additional security hardening is available</h3>\n<p>PyPI package maintainers can further increase the security of their release\nworkflows by configuring trusted publishers to only release from a specific\n<a href=\"https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment\">GitHub Actions environment</a>.</p>\n<p>Configuring an environment is optional, but strongly recommended: with a GitHub\nenvironment, you can apply additional restrictions to your trusted GitHub\nActions workflow, such as requiring manual approval on each run by a trusted\nsubset of repository maintainers.</p>\n<h3 id=\"unblocking-future-security-improvements\">Unblocking future security improvements</h3>\n<p>In addition to making publishing more secure now, the availability of trusted\npublishers unblocks additional future security improvements for PyPI.</p>\n<p>Configuring and using a trusted publisher provides a 'strong link' between a\nproject and its source repository, which can allow PyPI to verify related\nmetadata, like the URL of a source repository for a project<sup id=\"fnref:1\"><a class=\"footnote-ref\" href=\"#fn:1\">1</a></sup>. Additionally,\npublishing with a trusted publisher allows PyPI to correlate more information\nabout where a given file was published from in a verifiable way.</p>\n<p>Finally, although trusted publishers is currently limited to GitHub Actions,\nmuch of the underlying work that went into making this feature possible is\ngeneralizable and not specific to a single publisher. We're interested in\nsupporting the ability to publish from additional services that provide OpenID\nConnect identities.</p>\n<h3 id=\"get-started-today\">Get started today</h3>\n<p>To get started with using trusted publishers on PyPI, see our documentation\nhere: <a href=\"https://docs.pypi.org/trusted-publishers/\">https://docs.pypi.org/trusted-publishers/</a>.</p>\n<h3 id=\"acknowledgements\">Acknowledgements</h3>\n<p>Funding for this work was provided by the Google Open Source Security Team, and\nmuch of the development work was performed by <a href=\"https://www.trailofbits.com/\">Trail of Bits</a>, with special\nthanks to contributor <a href=\"https://github.com/woodruffw\">William Woodruff</a>.</p>\n<p>Many thanks as well to <a href=\"https://github.com/webknjaz\">Sviatoslav Sydorenko</a>, maintainer of the PyPA's\n<a href=\"https://github.com/marketplace/actions/pypi-publish\">'pypi-publish' GitHub Action</a> for his quick and timely work to add support for\ntrusted publishers in the action.</p>\n<p>Finally, we want to thank all our beta testers, including GitHub staff, for\nworking with us to ensure this feature is intuitive and useful, and for\nproviding valuable feedback to improve this feature along the way.</p>\n<hr />\n<p><em>Dustin Ingram is a maintainer of the Python Package Index.</em></p>\n<div class=\"footnote\">\n<hr />\n<ol>\n<li id=\"fn:1\">\n<p>Currently, information such as this are provided by the uploader and are not verified as accurate by PyPI.&#160;<a class=\"footnote-backref\" href=\"#fnref:1\" title=\"Jump back to footnote 1 in the text\">&#8617;</a></p>\n</li>\n</ol>\n</div>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-04-20-introducing-trusted-publishers.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-04-20T06:09:00+00:00", "authors": [{"name": "Dustin Ingram"}], "tags": ["oidc", "publishing", "security"]}, {"id": "https://blog.pypi.org/posts/2023-04-23-introducing-pypi-organizations/", "url": "https://blog.pypi.org/posts/2023-04-23-introducing-pypi-organizations/", "title": "Introducing PyPI Organizations", "content_html": "<p>Today, we are rolling out the first step in our plan to build financial\nsupport and long-term sustainability of the Python Packaging Index (PyPI),\nwhile simultaneously giving our users one of our most requested features:\norganization accounts.</p>\n<!-- more -->\n\n<h3 id=\"introducing-organizations\">Introducing Organizations</h3>\n<p>Organizations on PyPI are self-managed teams, with their own exclusive branded\nweb addresses. Our goal is to make PyPI easier to use for large community\nprojects, organizations, or companies who manage multiple sub-teams and multiple\npackages. We\u2019re making organizations available to community projects for free,\nforever, and to corporate projects for a small fee. Additional priority support\nagreements will be available to all paid subscribers, and all revenue will go\nright back into PyPI to continue building better support and infrastructure\nfor all our users.</p>\n<h3 id=\"increasing-sustainability-and-support\">Increasing sustainability and support</h3>\n<p>In the last year PyPI served 235.7 billion downloads for the 448,941 projects hosted\nthere. This means that since the previous period, PyPI saw a 57% annual growth in download counts\nand bandwidth alike. Having more people using and contributing to Python every\nyear is an fantastic problem to have, but it is one we must increase\norganizational capacity to accommodate.</p>\n<p>Increased revenue for PyPI allows it to become a\nstaffed platform that can respond to support requests and attend to issues\nin a timeframe that is significantly faster than what our excellent (but thinly\nspread) largely volunteer team could reasonably handle.</p>\n<h3 id=\"organizations-are-opt-in\">Organizations are opt-in</h3>\n<p>We want to be very clear\u2014these new features are completely optional. If\nfeatures for larger projects don't sound like something that would be useful to\nyou as a PyPI maintainer, then there is no obligation to create an organization\nand absolutely nothing about your PyPI experience will change for you.</p>\n<h3 id=\"a-basis-for-future-features\">A basis for future features</h3>\n<p>We look forward to discussing what other features PyPI users would like to see\ntackled next. We know there are lots of ideas out there around safety,\nsecurity and usability and we\u2019re looking forward to hearing about anything that\nyou think would benefit the community. And in the spirit of open source, <a href=\"https://github.com/pypi/warehouse/issues\">we\nwelcome your feedback</a> on what's on\noffer so far.</p>\n<h3 id=\"get-started-today\">Get started today</h3>\n<p>Both community projects (non-profits, NGO\u2019s, hobbyists, etc) and corporate\nteams can <a href=\"https://pypi.org/manage/organizations/\">sign up to request their organization name starting\ntoday</a>. Submissions will begin\nseeing review and approval in the coming weeks, and corporate teams will be\nable to finalize their signup with billing details in May.</p>\n<h3 id=\"acknowledgements\">Acknowledgements</h3>\n<p>Organization feature development was approved by the\n<a href=\"https://wiki.python.org/psf/PackagingWG\">Packaging Working Group</a>\nand funded through the\n<a href=\"https://www.python.org/psf-landing/\">Python Software Foundation</a>'s\nsponsorship program -- thanks to our <a href=\"https://pypi.org/sponsors/\">sponsors</a>\nfor making this work possible.</p>\n<p>We especially want to thank\n<a href=\"https://www.bloomberg.com/company/values/tech-at-bloomberg/?ea-publisher=psf\">Bloomberg</a>\nfor funding our Packaging Project Manager, Shamika Mohanan.</p>\n<p>We are also grateful for the many generous PyPI users who shared their\nperspectives with Shamika, which laid the foundation for these new features.</p>\n<p>And thanks as well to our beta\ntesters, including the following \u2728new\u2728 PyPI organizations:</p>\n<ul>\n<li>Python Cryptographic Authority (<a href=\"https://pypi.org/org/pyca/\">https://pypi.org/org/pyca/</a>)</li>\n<li>The Pallets Project (<a href=\"https://pypi.org/org/pallets/\">https://pypi.org/org/pallets/</a>)</li>\n<li>certifi (<a href=\"https://pypi.org/org/certifi\">https://pypi.org/org/certifi</a>)</li>\n</ul>\n<hr />\n<p><em>Ee Durbin is the Director of Infrastructure at\nthe Python Software Foundation.\nThey have been contributing to keeping PyPI online, available, and\nsecure since 2013.</em></p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-04-23-introducing-pypi-organizations.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-04-23T06:09:00+00:00", "authors": [{"name": "Ee Durbin"}], "tags": ["organizations", "sustainability"]}, {"id": "https://blog.pypi.org/posts/2023-05-09-announcing-pypi-safety-and-security-engr-role/", "url": "https://blog.pypi.org/posts/2023-05-09-announcing-pypi-safety-and-security-engr-role/", "title": "Announcing the PyPI Safety & Security Engineer role", "content_html": "<p>We are pleased to announce\nAmazon Web Services (AWS)\nas the inaugural Security Sponsor for PyPI,\ninvesting $144,000 over one year\nto fund key enhancements to PyPI infrastructure and operations,\nincluding the creation of a new \u201cPyPI Safety &amp; Security Engineer\u201d role. </p>\n<!-- more -->\n\n<p>This role builds on our existing long term partnership with AWS\nas one of the top sponsors of the Python Software Foundation for the last five years,\nwhich has included in-kind donations of cloud computing infrastructure\nand services to support PyPI.\nThe role will complement our previously announced role for a\n<a href=\"https://pyfound.blogspot.com/2023/01/the-psf-is-hiring-security-developer-in.html\">PSF Security Developer in Residence</a>\nand will work closely with the person hired for that role (to be announced soon).</p>\n<p>This funding also builds on\n<a href=\"https://dustingram.com/articles/2021/04/14/powering-the-python-package-index-in-2021/#project-funding\">previously successful project-focused funding efforts</a>,\nsuch as the 2018 full-stack rewrite of PyPI,\nthe introduction of internationalization and localization for PyPI,\nas well as 2FA and WebAuthn support.</p>\n<p>We expect this partnership to tangibly improve the experience for all PyPI users,\nfrom consumers downloading packages,\nto package maintainers,\nto large corporate teams.\nSome of the outcomes we are working toward over the next year include\nincreased support for package maintainers including multi-maintainer projects,\nimprovements to reporting infrastructure for malicious projects,\nas well as a reduced response time for malware reports and account recovery requests.</p>\n<p><a href=\"https://jobs.pyfound.org/apply/CKEONredws/PyPI-Safety-Security-Engineer\">The job posting can be found here</a>,\nand applications for the role are open until June 1st.\nSimilar to existing developer-in-residence roles,\nthe contract for this role will be for a one year period,\nand the PSF will be actively engaging with our sponsors and supporters\nto renew funding for the role in subsequent years.</p>\n<hr />\n<p><em>Ee Durbin is the Director of Infrastructure at\nthe Python Software Foundation.\nThey have been contributing to keeping PyPI online, available, and\nsecure since 2013.</em></p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-05-09-announcing-pypi-safety-and-security-engr-role.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-05-09T06:09:00+00:00", "authors": [{"name": "Ee Durbin"}], "tags": ["hiring", "security"]}, {"id": "https://blog.pypi.org/posts/2023-05-23-removing-pgp/", "url": "https://blog.pypi.org/posts/2023-05-23-removing-pgp/", "title": "Removing PGP from PyPI", "content_html": "<p>If you are someone who is currently uploading signatures, your package uploads will\ncontinue to succeed, but any PGP signatures will be silently ignored. If you are\nsomeone who is currently downloading PGP signatures, existing signatures\n<em>SHOULD</em> continue to be available <sup id=\"fnref:1\"><a class=\"footnote-ref\" href=\"#fn:1\">1</a></sup>, but no new signatures will be made available.\nThe related API fields such as <code>has_sig</code> have all been hardcoded to always be\n<code>False</code>.</p>\n<!-- more -->\n\n<p>Historically, PyPI has supported uploading PGP signatures alongside the release\nartifacts in an attempt to provide some level of package signing. However, the\napproach used had long standing,\n<a href=\"https://caremad.io/posts/2013/07/packaging-signing-not-holy-grail/\">documented issues</a>\nwhich had previously lead us to deemphasize the support\nfor PGP signatures over time by removing them from the PyPI web user interface.</p>\n<p>PyPI has continued to support uploading these signatures in the hope that there\nmight be some systems out there that found them useful. Recently though,\n<a href=\"https://blog.yossarian.net/2023/05/21/PGP-signatures-on-PyPI-worse-than-useless\">an examination of the signatures on PyPI</a>\nhas revealed to us that the current support for PGP signatures is not proving useful.</p>\n<p>In the last 3 years, about 50k signatures had been uploaded to PyPI by 1069\nunique keys. Of those 1069 unique keys, about 30% of them were not discoverable\non major public keyservers, making it difficult or impossible to meaningfully\nverify those signatures. Of the remaining 71%, nearly half of them were unable\nto be meaningfully verified at the time of the audit (2023-05-19) <sup id=\"fnref:2\"><a class=\"footnote-ref\" href=\"#fn:2\">2</a></sup>.</p>\n<p>In other words, out of all of the unique keys that had uploaded signatures to\nPyPI, only 36% of them were capable of being meaningfully verified <sup id=\"fnref:3\"><a class=\"footnote-ref\" href=\"#fn:3\">3</a></sup> at the\ntime of audit. Even if <em>all</em> of those signatures uploaded in that 3 year period\nof time were made by one of those 36% of keys that are able to be meaningfully\nverified, that would still represent only 0.3% of all of those files.</p>\n<p>Given all of this, the continued support of uploading PGP signatures to PyPI is\nno longer defensible. While it doesn't represent a <em>massive</em> operational burden\nto continue to support it, it does require any new features that touch the\nstorage of files to be made aware of and capable of handling these PGP\nsignatures, which is a non zero cost on the maintainers and contributors of\nPyPI.</p>\n<hr />\n<p><em>Donald Stufft is a PyPI administrator and maintainer of the Python Package Index since 2013.</em></p>\n<div class=\"footnote\">\n<hr />\n<ol>\n<li id=\"fn:1\">\n<p>For now, but they may be removed in the future.&#160;<a class=\"footnote-backref\" href=\"#fnref:1\" title=\"Jump back to footnote 1 in the text\">&#8617;</a></p>\n</li>\n<li id=\"fn:2\">\n<p>These could be because the original signature was made incorrectly and\n  never had a binding signature to a associated key identity, or because\n  the signature was present but had since expired.&#160;<a class=\"footnote-backref\" href=\"#fnref:2\" title=\"Jump back to footnote 2 in the text\">&#8617;</a></p>\n</li>\n<li id=\"fn:3\">\n<p>We use meaningfully verified to mean that the signature was valid and the\n  key that made it was not expired and had binding identify information that\n  could tell us if this key was the correct key.&#160;<a class=\"footnote-backref\" href=\"#fnref:3\" title=\"Jump back to footnote 3 in the text\">&#8617;</a></p>\n</li>\n</ol>\n</div>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-05-23-removing-pgp.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-05-23T06:09:00+00:00", "authors": [{"name": "Donald Stufft"}], "tags": ["security"]}, {"id": "https://blog.pypi.org/posts/2023-05-24-pypi-was-subpoenaed/", "url": "https://blog.pypi.org/posts/2023-05-24-pypi-was-subpoenaed/", "title": "PyPI was subpoenaed", "content_html": "<p>In March and April 2023, the Python Software Foundation (PSF)\nreceived three (3) subpoenas for PyPI user data.\nAll three subpoenas were issued by the United States Department of Justice.\nThe PSF was not provided with context on the legal circumstances surrounding these subpoenas.\nIn total, user data related to five (5) PyPI usernames were requested.</p>\n<!-- more -->\n\n<p>The data request was:</p>\n<ol>\n<li>\"Names (including subscriber names, user names, and screen names);\"</li>\n<li>\"Addresses (including mailing, residential addresses, business addresses, and email addresses);\"</li>\n<li>\"Connection records;\"</li>\n<li>\"Records of session times and durations, and the temporarily assigned network address (such as Internet Protocol addresses) associated with those sessions;\"</li>\n<li>\"Length of service (including start date) and type of services utilized;\"</li>\n<li>\"Telephone or instrument numbers (including the registration Internet Protocol address);\"</li>\n<li>\"Means and source of payment of any such services (including any credit card or bank account number) and billing records;\"</li>\n<li>\"Records of all Python Package Index (PyPI) packages uploaded by...\" given usernames</li>\n<li>\"IP download logs of any Python Package Index (PyPI) packages uploaded by...\" given usernames</li>\n</ol>\n<p>The privacy of PyPI users is of utmost concern to PSF and the PyPI Administrators,\nand we are committed to protecting user data from disclosure whenever possible.\nIn this case, however, PSF determined with the advice of counsel that\nour only course of action was to provide the requested data.\nI, as Director of Infrastructure of the Python Software Foundation,\nfulfilled the requests in consultation with PSF's counsel.</p>\n<p>We have waited for the string of subpoenas to subside, though we were committed\nfrom the beginning to write and publish this post as a matter of transparency,\nand as allowed by the lack of a non-disclosure order associated with the\nsubpoenas received in March and April 2023.</p>\n<h2 id=\"next-steps\">Next Steps</h2>\n<p>PyPI and the PSF are committed to the freedom, security, and privacy of our users.</p>\n<p>This process has offered time to revisit our current data and privacy standards,\nwhich are minimal, to ensure they take into account the varied interests of\nthe Python community.\nThough we collect very little personal data from PyPI users,\nany unnecessarily held data are still subject to these kinds of requests\nin addition to the baseline risk of data compromise via malice or operator error.</p>\n<p>As a result we are currently developing new data retention and disclosure policies.\nThese policies will relate to\nour procedures for future government data requests,\nhow and for what duration we store personally identifiable information such as\nuser access records,\nand policies that make these explicit for our users and community.</p>\n<p>Please continue to watch this blog for related announcements as policies\nare finalized, published, and implemented.</p>\n<hr />\n<h2 id=\"details\">Details</h2>\n<p>In order to provide as much transparency as possible,\nthe following will detail the shape of and extent of\nthe data that was contained in the responses to these subpoenas.</p>\n<p>We will not be releasing the usernames involved publicly\nor to the users themselves.</p>\n<h3 id=\"1-names-including-subscriber-names-user-names-and-screen-names\">1) Names (including subscriber names, user names, and screen names);</h3>\n<p>These were confirmed via our database records</p>\n<pre class=\"highlight\"><code class=\"language-sql\">select id, username, name from users where username = '{USERNAME}';</code></pre>\n<p>Returning:\n<pre class=\"highlight\"><code>id       | UUID for USERNAME\nusername | PyPI username\nname     | Display name for user</code></pre></p>\n<p>And are also publicly available at <code>https://pypi.org/user/{USERNAME}/</code>.</p>\n<p>PyPI allows users to delete their accounts.</p>\n<p>PyPI does not allow for the <code>username</code> field to be changed without admin intervention,\nand no such intervention has occurred for the users in question.\nIf they had, we would have provided records of those changes.</p>\n<p>PyPI does allow for user to update their display names\nand keeps no record of the history of these changes.</p>\n<h3 id=\"2-addresses-including-mailing-residential-addresses-business-addresses-and-email-addresses\">2) Addresses (including mailing, residential addresses, business addresses, and email addresses);</h3>\n<p>PyPI only stores email addresses for individual users. No physical addresses are stored.\nOrganization accounts who have signed up for billing (not yet live)\nwill be required to provide a billing address that validates to their payment method.</p>\n<p>These were sourced from our database records and is private to PyPI.</p>\n<pre class=\"highlight\"><code class=\"language-sql\">select email, user_id from user_emails where user_id ='{UUID for USERNAME}';</code></pre>\n<p>Returning:\n<pre class=\"highlight\"><code>email   | An email address\nuser_id | UUID for USERNAME</code></pre></p>\n<p>PyPI allows for users to add and remove email addresses without admin intervention.\nRecords of these changes are kept, and no such changes were observed in our records for\nthe usernames in question.\nIf they had, we would have provided records of those changes.</p>\n<h3 id=\"3-connection-records\">3. Connection records</h3>\n<h4 id=\"3a-project-events\">3a. Project events</h4>\n<p>PyPI retains records of all changes to projects on the index,\nand has since <code>2002-11-01 17:11:36 UTC</code>.</p>\n<p>These were confirmed via our database records</p>\n<pre class=\"highlight\"><code class=\"language-sql\">select * from journals where submitted_by='{USERNAME}' order by submitted_date;</code></pre>\n<p>Returning:\n<pre class=\"highlight\"><code>id             | An auto incrementing integer representing the \"Serial\"\nname           | Name of a PyPI Project\nversion        | Version of a PyPI Release if relevant\naction         | Description of the action taken against the Project/Release\nsubmitted_date | ISO-8601 datetime in UTC\nsubmitted_by   | PyPI Username\nsubmitted_from | IP Address</code></pre></p>\n<p>and with the exception of the <code>submitted_by</code> (PyPI username)\nand <code>submitted_from</code> (IP Address) columns\nare publicly available via our <a href=\"https://warehouse.pypa.io/api-reference/xml-rpc.html#changelog-since-with-ids-false\">XMLRPC API</a>.</p>\n<h4 id=\"3b-user-events\">3b. User events</h4>\n<p>PyPI retains records of critical user events including\naccount creation, emails sent, email address changes, logins, and login failures.\nSee <a href=\"https://github.com/pypi/warehouse/blob/9738ebb2ffcee91a935a6a11b224575aaf02a878/warehouse/events/tags.py#L61-L106\">this list</a>\nfor the comprehensive set of events recorded.</p>\n<p>These were sourced from our database records</p>\n<pre class=\"highlight\"><code class=\"language-sql\">select * from user_events where source_id = '{UUID for USERNAME}' order by time desc;</code></pre>\n<p>Returning:\n<pre class=\"highlight\"><code>id                | UUID of the event\nsource_id         | UUID for USERNAME\ntag               | EventTag\ntime              | ISO-8601 datetime in UTC\nip_address_string | IP Address\nadditional        | JSONB metadata about the event\nip_address_id     | UUID of associated IPAddress object</code></pre></p>\n<p>and are private to PyPI.</p>\n<h3 id=\"4-records-of-session-times-and-durations-and-the-temporarily-assigned-network-address-such-as-internet-protocol-addresses-associated-with-those-sessions\">4. Records of session times and durations, and the temporarily assigned network address (such as Internet Protocol addresses) associated with those sessions;</h3>\n<p>PyPI does not record session durations.</p>\n<p>Session creation (Login) times were provided as a synopsis of the data in 3b.</p>\n<p>Sessions are not created for uploads, but the associated login events\nfor project uploads were provided as a synopsis of the data in 3a.</p>\n<h3 id=\"5-length-of-service-including-start-date-and-type-of-services-utilized\">5. Length of service (including start date) and type of services utilized;</h3>\n<p>PyPI retains records of the date that a user account was created,\nas well as the last time it was successfully logged in by any method\n(web UI or command line tool for upload).</p>\n<pre class=\"highlight\"><code class=\"language-sql\">select date_joined, last_login from users where username =  {USERNAME}</code></pre>\n<p>Returning:\n<pre class=\"highlight\"><code>date_joined | ISO-8601 datetime in UTC\nlast_login  | ISO-8601 datetime in UTC</code></pre></p>\n<p>These were sourced from our database records and are private to PyPI.</p>\n<p>Types of service utilized are \"standard\" to PyPI and include the ability to\ncreate projects, releases, and distribution files for downloads.</p>\n<h3 id=\"6-telephone-or-instrument-numbers-including-the-registration-internet-protocol-address\">6. Telephone or instrument numbers (including the registration Internet Protocol address);</h3>\n<p>A synopsis of all IP Addresses for each username from previous records were shared.</p>\n<p>These were sourced from our database records and are private to PyPI.</p>\n<h3 id=\"7-means-and-source-of-payment-of-any-such-services-including-any-credit-card-or-bank-account-number-and-billing-records\">7. Means and source of payment of any such services (including any credit card or bank account number) and billing records;</h3>\n<p>PyPI has no cost to use for individual users\nand no such payment records or billing records exist.</p>\n<h3 id=\"8-records-of-all-python-package-index-pypi-packages-uploaded-by-the-given-usernames\">8. Records of all Python Package Index (PyPI) packages uploaded by the given usernames</h3>\n<p>A list of all past and any current projects associated with each username was provided.</p>\n<p>These were sourced from our database records and for past projects, are private to PyPI.</p>\n<h3 id=\"9-ip-download-logs-of-any-python-package-index-pypi-packages-uploaded-by-the-given-usernames\">9. IP download logs of any Python Package Index (PyPI) packages uploaded by the given usernames</h3>\n<p>PyPI does not retain download logs for packages which include IP addresses.\nDownload logs are processed by a pipeline which includes GeoIP information reported by our CDN only.</p>\n<p>These records were sourced from the <a href=\"https://docs.pypi.org/api/bigquery/\">Google BigQuery Public dataset</a> with the following queries:</p>\n<pre class=\"highlight\"><code class=\"language-sql\">SELECT * FROM `bigquery-public-data.pypi.file_downloads`\nWHERE project IN ({LIST OF PROJECT NAMES FROM 8})\nAND timestamp &gt; '{START OF PERIOD IN QUESTION}';</code></pre>\n<hr />\n<p><em>Ee Durbin is the Director of Infrastructure at\nthe Python Software Foundation.\nThey have been contributing to keeping PyPI online, available, and\nsecure since 2013.</em></p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-05-24-pypi-was-subpoenaed.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-05-24T06:09:00+00:00", "authors": [{"name": "Ee Durbin"}], "tags": ["compliance", "transparency"]}, {"id": "https://blog.pypi.org/posts/2023-05-25-securing-pypi-with-2fa/", "url": "https://blog.pypi.org/posts/2023-05-25-securing-pypi-with-2fa/", "title": "Securing PyPI accounts via Two-Factor Authentication", "content_html": "<p>One of the key security promises that PyPI makes is that when you're downloading\nsomething, that only the people associated with that project are going to be able\nto upload, delete, or otherwise modify a project. That when you look at that\nproject and see that it is owned by someone that you trust, that you can be\nassured that nobody else is making changes to that package on PyPI.</p>\n<!-- more -->\n\n<p>This promise is predicated on the security of each and every individual account\non PyPI used to create and maintain a Python project. In the past we've taken\nsteps to safeguard these accounts by\n<a href=\"https://caremad.io/posts/2018/08/pypi-compromised-passwords/\">blocking compromised passwords</a>, strong 2FA support using\n<a href=\"https://github.com/pypi/warehouse/pull/5567\">TOTP</a> and\n<a href=\"https://github.com/pypi/warehouse/pull/5795\">WebAuthN</a>,\n<a href=\"https://pypi.org/help/#apitoken\">support for API tokens with offline attenuation</a>,\n<a href=\"https://pypi.org/security-key-giveaway/\">enrolling the most downloaded projects into mandatory 2FA</a>,\nand <a href=\"https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/\">enabling short lived tokens for upload</a>.</p>\n<p>Today, as part of that long term effort to secure the Python ecosystem, we are\nannouncing that <em>every</em> account that maintains any project or organization\non PyPI will be required to enable 2FA on their account by the end of 2023.</p>\n<p>Between now and the end of the year, PyPI will begin gating access to certain\nsite functionality based on 2FA usage. In addition, we may begin selecting\ncertain users or projects for early enforcement.</p>\n<h2 id=\"what-can-i-do-to-prepare\">What can I do to prepare?</h2>\n<p>The most important things you can do to prepare are to enable 2FA for your\naccount as soon as possible, either with a\n<a href=\"https://pypi.org/help/#utfkey\">security device</a> (preferred) or an\n<a href=\"https://pypi.org/help/#totp\">authentication app</a> and to switch to using either\n<a href=\"https://docs.pypi.org/trusted-publishers/\">Trusted Publishers</a> (preferred) or\n<a href=\"https://pypi.org/help/#apitoken\">API tokens</a> to upload to PyPI.</p>\n<h2 id=\"why-use-2fa\">Why Use 2FA?</h2>\n<p>Account takeover attacks typically stem from someone using an insecure password:\nperhaps it was easy to guess, or it was reused and appeared in a breach. With\nthat insecure password, an attacker is able to gain control over a maintainers\naccount and begin to take actions as if they were that user.</p>\n<p>This is particularly problematic on a site like PyPI, where the actions that a\nperson can take include releasing software that might be used by people world\nwide, allowing an attacker to install and execute software on unsuspecting\nuser's computers. <a href=\"https://python-security.readthedocs.io/pypi-vuln/index-2022-05-24-ctx-domain-takeover.html\">Account takeover attacks have been previously used to\ncompromise PyPI users in this\nway</a>.</p>\n<p>Two-factor authentication immediately neutralizes the risk associated with a\ncompromised password. If an attacker has someone's password, that is no longer\nenough to give them access to that account.</p>\n<h2 id=\"why-every-project-or-organization-maintainer\">Why every project or organization maintainer?</h2>\n<p>There's two ways to think about this question:</p>\n<ul>\n<li>Why every project and organization maintainer instead of just some subset of\n  them (based on popularity, purpose, whether that user uses their account,\n  etc)?</li>\n<li>Why only maintainers and not every single user?</li>\n</ul>\n<p>Not every account on PyPI has the same value to an attacker. An account with\naccess to the most downloaded project on PyPI can be used to attack far more\npeople than an account with access to the least downloaded project.</p>\n<p>However, it only takes one compromised project in someone's dependency set to\ncompromise their computer. The attacker doesn't care if they get you from a\nwidely used or a niche project, just that they got you. Even worse, once\ncompromised, an attacker can extend that attack to attack <em>other</em> systems,\nincluding other projects on PyPI that the now compromised person maintains.</p>\n<p>Given that it only takes one compromised project, no matter how many downloads\nit gets <sup id=\"fnref:1\"><a class=\"footnote-ref\" href=\"#fn:1\">1</a></sup>, to compromise someone we want to ensure that every project is\nbeing protected by 2FA.</p>\n<p>On the flipside, an account without access to any project cannot be used to\nattack anyone <sup id=\"fnref:2\"><a class=\"footnote-ref\" href=\"#fn:2\">2</a></sup> so it is a very low value target.</p>\n<p>We recognize that enabling 2FA for an account imposes a non zero cost both for\nthe owner of that account <em>and</em> for PyPI <sup id=\"fnref:3\"><a class=\"footnote-ref\" href=\"#fn:3\">3</a></sup>, so forcing that on accounts that\ncannot affect anyone but themselves is not an effective use of limited\nresources. In addition, from a practical view, the standard 2FA flow that most\npeople are used to and that PyPI implements also involves adding a 2FA token\nto an existing account rather than forcing it to be part of the registration\nflow.</p>\n<p>Our expectation is that for users who currently are not a project maintainer or\norganization member, the ultimate experience will be whenever they attempt to\ntake some action that would require them to add 2FA (creating a new project,\naccepting an invite, making an organization, etc) they will be prompted to add\n2FA to their account before they can proceed.</p>\n<h2 id=\"why-now\">Why now?</h2>\n<p>The direction of many projects in or serving the Open Source community in the\nlast 5-10 years has been an increasing focus on supply chain security,\npreventing attacks that are being delivered through the \"supply chain\", namely\nthe services and tooling used to create and consume software.</p>\n<p>In July of 2022, we announced\n<a href=\"https://pypi.org/security-key-giveaway/\">a security key giveway</a> in conjunction\nwith a plan to begin mandating 2FA for the top 1% of projects on PyPI by download\ncount.</p>\n<p>The initial announcement of that mandate to the top 1%\nof projects and the related giveaway was met with mixed reactions, ranging from\npeople applauding the effort to people deciding to distribute their code on\nplaces other than PyPI. We planned to limit this to the projects in the top 1%\nof downloads because those are likely he highest value targets for an attacker,\nand because we were concerned about the impact of such a mandate on both the\nmaintainers of projects on PyPI, and on the support burden of the PyPI team\nitself.</p>\n<p>At that time last year, we did not have any plans or expectations on widening\nour net of users that would fall under that mandate, other than would occur\nnaturally as projects rose in popularity to be part the top 1%.</p>\n<p>However, in the year since then a few things have changed.</p>\n<ul>\n<li>We've gotten a lot more confident in our 2FA implementation and in what the\n  impact to enabling it is for both people publishing to PyPI, and to the PyPI\n  team itself.</li>\n<li>We've shipped features like\n  <a href=\"https://docs.pypi.org/trusted-publishers/\">Trusted Publishing</a> that help\n  remove some of the overheard of 2FA has on publishing (by far the most common\n  action users do).</li>\n<li>GitHub has furthered it's\n  <a href=\"https://github.blog/2022-05-04-software-security-starts-with-the-developer-securing-developer-accounts-with-2fa/\">plans to mandate 2FA for all contributors</a>\n  on their platform, making more people already (or soon will be) prepared to cope\n  with the requirements of having 2FA.</li>\n<li>The PSF has received funding to hire a\n   <a href=\"https://blog.pypi.org/posts/2023-05-09-announcing-pypi-safety-and-security-engr-role/\">PyPI Safety and Security Engineer</a>.\n  While the role is not meant to purely handle support requests, having\n  dedicated focus on the overall security posture, as well as the\n  implementation specifics will improve PyPI as a whole for both\n  users and project maintainers, and will help alleviate some of the\n  pressures on PyPI's volunteers.</li>\n<li>The workload to support end users relies heavily on a very small group of\n  volunteers. When an user account report is seen by our trusted admins, we have\n  to take time to properly investigate. These are often reported as an emergency,\n  red-alert-level urgency. By mandating 2FA for project maintainers, the\n  likelihood of account takeovers drop significantly, reserving the emergency\n  status for truly extraordinary circumstances. Account recovery becomes part of\n  normal routine support efforts instead of admin-level urgency.</li>\n</ul>\n<p>All of these together help lead us to the conclusion that we can widen our\nmandate to <em>all</em> project maintainers on PyPI, while minimizing the impact on\nboth project maintainers and PyPI administrators, and to do so in a way that\nimproves the sustainability of PyPI and the wider Python ecosystem.</p>\n<h2 id=\"isnt-supply-chain-security-a-corporate-concern\">Isn't supply chain security a corporate concern?</h2>\n<p>There are some people who believe that efforts to improve supply chain security\nbenefits only corporate or business users, and that individual developers should\nnot be asked to take on a uncompensated burden for their benefit.</p>\n<p>We believe this is shortsighted.</p>\n<p>A compromise in the supply chain can be used to attack individual developers the\nsame as it able to attack corporate and business users. In fact, we believe\nthat individual developers, are in a <em>more</em> vulnerable position than corporate\nand business users. While businesses are generally able to hire staff and devote\nresources to vetting their dependencies, individual developers generally are\nnot, and must expend their own limited free time to do so <sup id=\"fnref:4\"><a class=\"footnote-ref\" href=\"#fn:4\">4</a></sup>.</p>\n<p>To make matters worse for the individual, in the case of a compromise a business\nis more likely going to have experts available to them to detect and remediate\nthe compromise, while the individual has to do this on their own. At the extreme\nends, businesses often have insurance to compensate them for any losses incurred\nwhile the individual almost always does not.</p>\n<p>We recognize that supply chain security effects <em>everyone</em>, no matter how big\nor how small they are, and we are dedicated to protecting <em>all</em> our users.</p>\n<hr />\n<p><em>Donald Stufft is a PyPI administrator and maintainer of the Python Package Index since 2013.</em></p>\n<div class=\"footnote\">\n<hr />\n<ol>\n<li id=\"fn:1\">\n<p><em>Technically</em> a project with 0 downloads is effectively the same as a\n  non-existent project, but it's easier to draw the line between\n  non-existent and existent than it is to draw the line between 0 and 1\n  downloads. This is particularly true on PyPI, where a large network of\n  mirrors and scanners mean that <em>no</em> projects truly get downloaded exactly\n  0 times.&#160;<a class=\"footnote-backref\" href=\"#fnref:1\" title=\"Jump back to footnote 1 in the text\">&#8617;</a></p>\n</li>\n<li id=\"fn:2\">\n<p>Except maybe the account owner themselves, by denying them access to their\n  account.&#160;<a class=\"footnote-backref\" href=\"#fnref:2\" title=\"Jump back to footnote 2 in the text\">&#8617;</a></p>\n</li>\n<li id=\"fn:3\">\n<p>For end users it forces them to purchase some kind of hardware token <em>OR</em>\n  to use some sort of TOTP application. In both cases it forces them to keep\n  track of something else besides their password and changes the login flow\n  from what they are used to. For PyPI it increases the chance that someone\n  may get locked out of their account, requiring intervention by administrators.&#160;<a class=\"footnote-backref\" href=\"#fnref:3\" title=\"Jump back to footnote 3 in the text\">&#8617;</a></p>\n</li>\n<li id=\"fn:4\">\n<p>Not for nothing, but PyPI is also an Open Source project, run largely by\n  volunteers, and cleaning up after a compromise on PyPI is something that\n  affects those volunteers significantly.&#160;<a class=\"footnote-backref\" href=\"#fnref:4\" title=\"Jump back to footnote 4 in the text\">&#8617;</a></p>\n</li>\n</ol>\n</div>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-05-25-securing-pypi-with-2fa.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-05-25T06:09:00+00:00", "authors": [{"name": "Donald Stufft"}], "tags": ["2fa", "security"]}, {"id": "https://blog.pypi.org/posts/2023-05-26-reducing-stored-ip-data/", "url": "https://blog.pypi.org/posts/2023-05-26-reducing-stored-ip-data/", "title": "Reducing Stored IP Data in PyPI", "content_html": "<p>Hi there! I'm Mike, the newest member of the PyPI admin team. Nice to meet you!</p>\n<h2 id=\"tldr\">TL;DR</h2>\n<p>We've been working on reducing the amount of IP address data we store,\nand we're making progress.</p>\n<!-- more -->\n\n<h2 id=\"whats-this-about\">What's this about?</h2>\n<p>If you've read some of the other blogs here, you may have noticed that\nwe've been working on a number of security and privacy improvements.</p>\n<p>A few months ago we started exploring what it would take\nto remove the concept of IP addresses from our stack,\nand retain the ability to safely manage the platform.</p>\n<p>Some places of where IP data was historically used:</p>\n<ul>\n<li>web access logs</li>\n<li>user events (login,. logout, password reset, et al)</li>\n<li>project events (creation, new releases, new file uploads, yanks, et al)</li>\n<li>organization/team membership events (<a href=\"../2023-04-23-introducing-pypi-organizations/\">new!</a>)</li>\n<li>journal entries (private to PyPI admins)</li>\n</ul>\n<p>Security is a spectrum - where on one extreme, it's the wild west, no security.\nOn the other extreme, everything is locked down, impossible to use.\nWe constantly have to balance the needs and desires of our users\nwith the needs of running a sustainable, trusted platform.</p>\n<p>A similar mindset can be applied to privacy - where we must strike the balance\nbetween providing a manageable service, and protecting the privacy of our users.</p>\n<p>The two main approaches we've pursued in the short term are:</p>\n<ul>\n<li>Evaluate whether we need to store IP data at all</li>\n<li>Whenever possible, use alternatives to IP data</li>\n</ul>\n<p>I'll provide a couple of examples demonstrating the above.</p>\n<h2 id=\"do-we-need-the-ip-data\">Do we need the IP data?</h2>\n<p>As we evaluated the different places we stored IP data,\nwe learned that our Journal entries (similar to an append-only transaction log)\nwere never exposed beyond admin users, and even then, used for admin display only.</p>\n<p>We never share that via API, or used it for operational purposed.\nSo we audited the code, removed calls to stop writing the column, and dropped it.</p>\n<p>Woohoo!</p>\n<p>Other places where we currently still need IP data include rate limiting,\nand fallbacks until we have backfilled the IP data with hashes and geo data.\nOur modern approach has evolved from using the IP data at display time to find\nthe relevant geo data, to storing the geo data directly in the database.</p>\n<p>Another use case is for handling abuse - we need to be able to identify\nthe source of abuse, and take action to prevent it.\nWe're thinking about how to manage that without storing IP data,\nbut we're not there yet.</p>\n<h2 id=\"alternatives-to-ip-data\">Alternatives to IP data</h2>\n<p>For the other places where we stored IP data,\nwe asked ourselves - could we use an alternative?</p>\n<p>We can't store what we don't see, so we explored what we could do to\nreduce the amount of IP data we see.</p>\n<p>Pretty much every request to PyPI is served via our CDN partner, Fastly.\nThey provide a number of features, including the ability to\n<a href=\"https://docs.fastly.com/en/guides/adding-or-modifying-headers-on-http-requests-and-responses\">add custom headers</a>.\nWe leverage this ability already for a number of things,\nlike informing the warehouse code of the inbound encoding of the request or language.</p>\n<p>We explored whether we could use this to add a hash of the IP address,\nand use that instead of the IP address itself.\nFastly can also provide some basic geographic info about the IP address,\nwhich served our purpose of showing the user where they had connected from.</p>\n<p>Using this approach, we have Fastly pass along an already-hashed IP address,\nas well as the geographic data, to our backend, and store those for later use.</p>\n<p>Another spot we identified was web access logs.\nWe don't need real IP addresses there,\nas we rarely use them for anything other than low-level debugging,\nso a stable, hashed value serves the purpose of correlating requests.</p>\n<p>For the requests that Fastly doesn't serve, we're already hashing the IP address\nourselves prior to storage, so we could \"see\" the IP address briefly,\nbut we try to avoid storing it.\nWe don't get get the geo data for these requests,\nwe're thinking of creative and sustainable solutions already.</p>\n<h2 id=\"questions-and-answers\">Questions and Answers</h2>\n<p>I tried to think up some questions you might have, and answer them here.\nIf you have more, please feel free to reach out to us!</p>\n<p><strong>Q:</strong> Is a hashed IP address secure?</p>\n<p><strong>A:</strong> It's more secure than a plain IP address, for sure.\nWe apply a <a href=\"https://en.wikipedia.org/wiki/Salt_(cryptography)\">salt</a> (known value)\nto the IP address before hashing it.\nIt's not a perfect solution, but it's a step in the right direction.</p>\n<p>The hash is non-reversible, but since the known address space is relatively small,\nit's possible to brute force the hash to determine the original IP address.\nBy applying a salt, we require someone to possess <strong>both</strong> the salt\nand the hashed IP addresses to brute force the value.\nOur salt is not stored in the database while the hashed IP addresses are,\nwe protect against leaks revealing this information.</p>\n<p><strong>Q:</strong> Is this a response to the subpoenas?</p>\n<p><strong>A:</strong> No, we started exploring this <a href=\"https://github.com/pypi/warehouse/issues/8158\">back in 2020</a>,\nwith a long term goal to increase end-user security,\nwhile retaining the ability to effectively steer the platform.\nWe picked it up recently as we explored our CDN partner's options for geo IP data.</p>\n<p><strong>Q:</strong> Are we done?</p>\n<p><strong>A:</strong> Nope! Security is an ongoing journey,\nand we're making strong strides to accomplish this goal.\nWe still have some work to do to replace IP data in our models,\nafter we've backfilled our models with the hashed IP data and relevant geo data,\nand clean up some of the code.</p>\n<p><strong>Q:</strong> What's next?</p>\n<p><strong>A:</strong> I can't predict every future step we're likely to take,\nbut some things we're considering:</p>\n<ul>\n<li>Reevaluate the need for IP data in Event history <strong>forever</strong>,\n  remove it after a period of time</li>\n<li>Explore whether we can use a CDN for all requests</li>\n<li>Determine if there's a better mechanism than Journal Entries, and replace them</li>\n</ul>\n<p>We believe the steps we're taking are in the right direction,\nand we're excited to share our progress with you.\nHopefully this enriches your understanding of the work we're doing,\nin support of maintaining a secure, trusted platform.</p>\n<p>Thanks for reading!</p>\n<hr />\n<p><em>Mike Fiedler is a PyPI administrator\nand maintainer of the Python Package Index since 2022.</em></p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-05-26-reducing-stored-ip-data.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-05-26T15:00:00+00:00", "authors": [{"name": "Mike Fiedler"}], "tags": ["security", "transparency"]}, {"id": "https://blog.pypi.org/posts/2023-06-01-2fa-enforcement-for-upload/", "url": "https://blog.pypi.org/posts/2023-06-01-2fa-enforcement-for-upload/", "title": "Enforcement of 2FA for upload.pypi.org begins today", "content_html": "<p>Beginning today, all uploads from user accounts with 2FA enabled\nwill be required to use an <a href=\"https://pypi.org/help/#apitoken\">API Token</a>\nor <a href=\"https://docs.pypi.org/trusted-publishers/\">Trusted Publisher</a> configuration\nin place of their password.</p>\n<!-- more -->\n\n<p>This change has <a href=\"https://github.com/pypi/warehouse/issues/7265\">been planned</a>\nsince 2FA was rolled out in 2019.\nIn <a href=\"https://github.com/pypi/warehouse/pull/10836\">February of 2022</a>\nwe began notifying users on upload that this change was coming.</p>\n<p>If you have 2FA enabled and have been using only your password to upload,\nthe following email is likely familiar to you:</p>\n<figure>\n<p><img alt=\"Sample notice email\" src=\"../../assets/2023-06-01-2fa-notice-email.png\" />\n  </p>\n<figcaption>\n      A sample notice email sent when users with 2FA enabled\n      upload using only their password.\n  </figcaption>\n</figure>\n<p>Initially, we intended for this notice to live for six months before\nwe began enforcement.</p>\n<p>However, some valid concerns were raised regarding\nthe use of user-scoped API tokens for new project creation.</p>\n<p>With the <a href=\"../2023-04-20-introducing-trusted-publishers/\">introduction of Trusted Publishers</a>\nPyPI now provides a way for users to publish <strong>new</strong> projects without\nprovisioning a user-scoped token, and to continue publishing without\never provisioning a long lived API token whatsoever.</p>\n<p>Given this, and our <a href=\"../2023-05-25-securing-pypi-with-2fa/\">commitment to further rolling out 2FA across PyPI</a>,\nwe are now enforcing this policy.</p>\n<hr />\n<p><em>Ee Durbin is the Director of Infrastructure at\nthe Python Software Foundation.\nThey have been contributing to keeping PyPI online, available, and\nsecure since 2013.</em></p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-06-01-2fa-enforcement-for-upload.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-06-01T06:09:00+00:00", "authors": [{"name": "Ee Durbin"}], "tags": ["2fa", "security"]}, {"id": "https://blog.pypi.org/posts/2023-06-22-malware-detection-project/", "url": "https://blog.pypi.org/posts/2023-06-22-malware-detection-project/", "title": "Announcing the launch of PyPI Malware Reporting and Response project", "content_html": "<p>We are pleased to announce that the PSF has received funding \nfrom the <a href=\"https://cset.georgetown.edu/\">Center for Security and Emerging Technology</a> \n(CSET) to develop and improve the infrastructure for \nmalware reporting and response on PyPI. This project will be executed over the coming year.</p>\n<!-- more -->\n\n<p>Currently, malware reports are submitted to PyPI admins by email before \nbeing manually triaged and responded to. There is an opportunity for \nimprovement in streamlining the report submission process and the tools \nused to triage and respond to them. The current process cannot scale \neasily or handle duplication of reports. It is not easy to measure \ntime to remediation and is currently impossible to implement \nautomated takedown of threats.</p>\n<p>This project has the following aims:</p>\n<ul>\n<li>Develop an API that allows malware reporting</li>\n<li>Extend PyPI admin tools to view, collate and handle security reports</li>\n<li>Collect metadata as required and identify trusted reporters</li>\n<li>Define metrics that allow us to define good reporting practices \nand time to handle a security issue</li>\n<li>Define the criteria for automated consensus based takedown and \nsoft-deletes of packages</li>\n<li>Highlight trusted reporters and report quality</li>\n</ul>\n<p>As PyPI is an integral part of the Python ecosystem, this project \nis crucial in ensuring the security of over 450,000 packages that \nare trusted by millions of Python developers. Over the next few \nweeks, we will be working with security reporters to identify \nkey elements that should be supported by the API and useful \nmetrics that would add value to PyPI security reporting. If \nyou or your colleagues are currently performing malware \nanalysis of PyPI uploads, we would love to hear from you \nat <a href=\"https://forms.gle/ixRoNJEPVNekFN7H7\">https://forms.gle/ixRoNJEPVNekFN7H7</a>.</p>\n<hr />\n<p><em>Shamika Mohanan is the Packaging Project Manager at the PSF \nsince 2021.</em></p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-06-22-malware-detection-project.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-06-22T16:00:00+00:00", "authors": [{"name": "Shamika Monahan"}], "tags": ["security"]}, {"id": "https://blog.pypi.org/posts/2023-06-26-deprecate-egg-uploads/", "url": "https://blog.pypi.org/posts/2023-06-26-deprecate-egg-uploads/", "title": "Deprecation of bdist_egg uploads to PyPI", "content_html": "<p><a href=\"https://peps.python.org/pep-0715/\">PEP 715</a>, deprecating <code>bdist_egg</code>/<code>.egg</code>\nuploads to PyPI has been\n<a href=\"https://discuss.python.org/t/pep-715-disabling-bdist-egg-distribution-uploads-on-pypi/27610/13\">accepted</a>.\nWe'll begin the process of implementing this today.</p>\n<p>Please note that this does <strong>NOT</strong> remove any existing uploaded eggs from PyPI.</p>\n<!-- more -->\n\n<p>The deprecation timeline is as follows:</p>\n<ul>\n<li>Today, June 26, 2023: All maintainers of projects which have uploaded one or\n  more eggs since January 1, 2023 will receive a one-time email informing them\n  of this change.</li>\n<li>Today, June 26, 2023: Each upload of an egg to PyPI will result in a notice\n  being sent to all Owners and Maintainers for the project.</li>\n<li>August 1, 2023: Uploads of eggs will be <a href=\"https://i.kym-cdn.com/photos/images/original/001/402/192/398.jpg\"><strong>rejected</strong></a> by PyPI.</li>\n</ul>\n<p>You can read more detailed rationale in <a href=\"https://peps.python.org/pep-0715/#rationale\">PEP 715</a>.\nThanks to contributor <a href=\"https://blog.yossarian.net\">William Woodruff</a> for his\nwork to author and propose PEP 715, as well as support the rollout of the\nimplementation.</p>\n<hr />\n<p><em>Ee Durbin is the Director of Infrastructure at\nthe Python Software Foundation.\nThey have been contributing to keeping PyPI online, available, and\nsecure since 2013.</em></p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-06-26-deprecate-egg-uploads.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-06-26T06:09:00+00:00", "authors": [{"name": "Ee Durbin"}], "tags": ["deprecation"]}, {"id": "https://blog.pypi.org/posts/2023-08-04-pypi-hires-safety-engineer/", "url": "https://blog.pypi.org/posts/2023-08-04-pypi-hires-safety-engineer/", "title": "PyPI hires a Safety & Security Engineer", "content_html": "<p>\ud83d\udc4b Hi there! I'm Mike Fiedler (<a href=\"https://github.com/miketheman\">@miketheman</a>)\nI've been a Python Package Index (<a href=\"https://pypi.org/\">PyPI</a>) contributor since early 2021, and became a maintainer in 2022.\nNow I'm joining the <a href=\"https://www.python.org/psf-landing/\">PSF</a> to work on PyPI full-time as the first PyPI Safety &amp; Security Engineer.</p>\n<!-- more -->\n\n<p>What is that, you ask?\nWe had <a href=\"../2023-05-09-announcing-pypi-safety-and-security-engr-role/\">posted about this opening in May</a>,\nand I'm happy to be joining the team to help improve the safety and security of PyPI.</p>\n<p>What does that mean, Mike?\nAs 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's a huge audience!</p>\n<p>All of us deserve to have a safe and secure experience when using PyPI, and I'm excited to be able to work on this full-time.</p>\n<p>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.</p>\n<hr />\n<p><em>Mike Fiedler is the inaugural PyPI Safety &amp; Security Engineer since 2023.</em></p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-08-04-pypi-hires-safety-engineer.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-08-05T06:09:00+00:00", "authors": [{"name": "Mike Fiedler"}], "tags": ["hiring"]}, {"id": "https://blog.pypi.org/posts/2023-08-08-2fa-enforcement-for-new-users/", "url": "https://blog.pypi.org/posts/2023-08-08-2fa-enforcement-for-new-users/", "title": "2FA Enforcement for New User Registrations", "content_html": "<h2 id=\"whats-changing\">What's changing?</h2>\n<p>Starting today, <strong>newly registered users must enable 2FA\nbefore they can perform any management actions on PyPI</strong>.\nThis change comes after we've also added a rule for accounts to have\na verified, <em>primary</em> email address for the same set of management actions.</p>\n<p>As a reminder, PyPI has supported adding 2FA since 2019.</p>\n<!-- more -->\n\n<p>This change is continuing along the path of enforcing 2FA for all users.\nIn May of this year we <a href=\"../2023-05-25-securing-pypi-with-2fa/\">announced</a>\nthat by the end of 2023 PyPI will require all users to enable Two-Factor Authentication (2FA).\nThat post has a wealth of information on what enforcement means,\nand how folks can prepare for the change before end of year.</p>\n<h2 id=\"what-are-management-actions\">What are management actions?</h2>\n<p>Management actions may include any of the following:</p>\n<ul>\n<li>Creating/managing Projects</li>\n<li>Adding/removing API Tokens</li>\n<li>Uploading/removing Releases</li>\n<li>Adding/removing Collaborators</li>\n<li>Requesting/managing <a href=\"../2023-04-23-introducing-pypi-organizations/\">Organizations</a></li>\n<li>Adding/managing <a href=\"../2023-04-20-introducing-trusted-publishers/\">Trusted Publishers</a></li>\n</ul>\n<p>This is not an exhaustive list,\nbut should provide a good idea of the actions we're talking about.</p>\n<h2 id=\"how-does-this-affect-me\">How does this affect me?</h2>\n<p>If you only need to browse, download, and install packages from PyPI\nthen a PyPI account isn't needed so this change doesn't affect you.</p>\n<p>If you've already enabled 2FA on your PyPI account,\nthis change will not affect you.\nThanks for doing your part to keep the Python ecosystem safe!</p>\n<p>If you recently registered a new PyPI account,\nyou are required to enable 2FA before you can perform any management actions.\nWhen attempting to perform a management action,\nyou may see a red banner flash at the top of the page,\nand be redirected to the 2FA setup page for your account.</p>\n<p>You will still be able to log in, browse, and download packages without 2FA.\nBut to perform any management actions, you'll need to enable 2FA.</p>\n<h2 id=\"is-this-the-end\">Is this the end?</h2>\n<p>As a reminder, we will enforce the 2FA requirement for all PyPI users\nat the end of 2023.</p>\n<p>These changes intend to mitigate scenarios like account takeovers,\nwhere an attacker may be able to gain access to a user's account\nusing only an email and password (either via phishing or credential stuffing).\nIf a user's email account access is compromised,\nand the attacker is able to successfully request a password reset,\nthe attacker would still need to bypass the 2FA requirement.</p>\n<p>The more users using a Two Factor Authentication methods available,\nthe safer we all are.\nToday PyPI offers both <a href=\"https://en.wikipedia.org/wiki/Time-based_one-time_password\">Time-based One-Time Passwords (TOTP)</a>\nand <a href=\"https://en.wikipedia.org/wiki/WebAuthn\">WebAuthn</a>.</p>\n<p>Security is a spectrum.\nAs long as we continue to make incremental progress,\nwe'll improve the overall posture for <strong>all users</strong> of PyPI.</p>\n<hr />\n<p><em>Mike Fiedler is the PyPI Safety &amp; Security Engineer since 2023.</em></p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-08-08-2fa-enforcement-for-new-users.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-08-08T06:09:00+00:00", "authors": [{"name": "Mike Fiedler"}], "tags": ["2fa", "security"]}, {"id": "https://blog.pypi.org/posts/2023-08-17-github-token-scanning-for-public-repos/", "url": "https://blog.pypi.org/posts/2023-08-17-github-token-scanning-for-public-repos/", "title": "GitHub now scans public issues for PyPI secrets", "content_html": "<p>Back in 2019 we <a href=\"https://github.com/pypi/warehouse/issues/6051\">kicked off efforts</a>\nto integrate with GitHub secret scanning.\nDue to the complexity in nature, the completed integration launched in 2021,\nwith the volunteer-led effort by Joachim Jablon (<a href=\"https://github.com/ewjoachim\">@ewjoachim</a>)\nand the GitHub team.</p>\n<!-- more -->\n\n<p>PyPI didn't have a blog back then, but GitHub did!\nHere's <a href=\"https://github.blog/changelog/2021-03-22-the-python-package-index-is-now-a-github-secret-scanning-integrator/\">a link their post</a>.</p>\n<p>The completed integration increased security for all PyPI users.\nIf a user accidentally made their PyPI token public by committing it\nand pushing it to a GitHub public repository,\nGitHub would notify us and we would automatically revoke the token to prevent any misuse.\nThis process often completes within seconds.</p>\n<p><strong>Cool, Mike, that was two years ago, so what?</strong></p>\n<p>GitHub <a href=\"https://github.blog/changelog/2023-08-16-secret-scanning-detects-secrets-in-issues-for-free-public-repositories/\">announced yesterday</a> that they will now notify users about any secrets exposed in:</p>\n<blockquote>\n<p>... issue's title, description, or comments, including historical revisions ...</p>\n</blockquote>\n<p>This is a great enhancement to their existing secret scanning capabilities,\npreviously only scanning <strong>code</strong>.</p>\n<p>And even better, we don't have to change anything,\nas we continue to receive the same notifications from our existing integration with GitHub. \ud83c\udf89</p>\n<p>When implementing the integration,\nwe set up some metrics to track the number of tokens revoked.\nHere's a visualization showing the amount of inbound notifications from GitHub,\nand the number of tokens we've revoked <em>(click for larger image)</em>:</p>\n<figure>\n<p><a href=\"../../assets/dd-gh-token-scanning_2023-08-16_18.12.26.png\"><img alt=\"GitHub secret scanning metrics\" src=\"../../assets/dd-gh-token-scanning_2023-08-16_18.12.26.png\" /></a>\n  </p>\n<figcaption>Bar charts of values over period of 1 year, and total counts of: received (5.87k), valid (537), processed (535)</figcaption>\n</figure>\n<p>We show 15 months of data,\nthanks to our infrastructure sponsor <a href=\"https://www.datadoghq.com/\">Datadog</a>.</p>\n<p>The large discrepancy between \"received\" and \"valid\" can be due to\nGitHub reporting the same token multiple times.\nThis can happen if the token is used in multiple repositories, forks, etc.</p>\n<p>The small discrepancy between \"valid\" and \"processed\" is due to that\nwe may raise an exception when attempting to email the owner of a token that has been revoked, after which we don't mark it as \"processed\" when exception is raised.\nThe second time the task runs, the token is now invalid, and doesn't increment the \"valid\" count.</p>\n<p>Read <a href=\"https://github.blog/changelog/2023-08-16-secret-scanning-detects-secrets-in-issues-for-free-public-repositories/\">GitHub's notice on The Changelog</a>\nfor more details.</p>\n<p>If your organization performs this kind of secret scanning,\nwe'd love to hear from you and integrate with your tooling.</p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-08-17-github-token-scanning-for-public-repos.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-08-17T06:09:00+00:00", "authors": [{"name": "Mike Fiedler"}], "tags": ["integrations", "security"]}, {"id": "https://blog.pypi.org/posts/2023-09-18-inbound-malware-reporting/", "url": "https://blog.pypi.org/posts/2023-09-18-inbound-malware-reporting/", "title": "Inbound Malware Volume Report", "content_html": "<h2 id=\"background\">Background</h2>\n<p>The current <a href=\"https://pypi.org/security/\">PyPI security reporting procedure</a>\ndirects reporters to send an email to <a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#115;&#101;&#99;&#117;&#114;&#105;&#116;&#121;&#64;&#112;&#121;&#112;&#105;&#46;&#111;&#114;&#103;\">&#115;&#101;&#99;&#117;&#114;&#105;&#116;&#121;&#64;&#112;&#121;&#112;&#105;&#46;&#111;&#114;&#103;</a> with details.\n<code>security@</code> was previously an email alias for <code>admin@</code>,\na Google Group that contains all current PyPI Administrators (4 people).</p>\n<!-- more -->\n\n<p>I'll refer to the <code>security@</code> address as the <strong>Security Inbox</strong> herein,\ndespite it not being a traditional inbox, and what changes we've made to it.</p>\n<p>The inbound reporting workflow was roughly:</p>\n<ul>\n<li>Admins receive an email to the Security Inbox</li>\n<li>Any admin reads the email</li>\n<li>Admin inspects the indicators of compromise (IOC), package and user history.\n  Often using <a href=\"https://inspector.pypi.io/\">inspector.pypi.io</a> to investigate package contents</li>\n<li>Admin takes an action (oftentimes to remove as malware)</li>\n<li>Admin responds to reporter and CCs the Security Inbox for tracking</li>\n</ul>\n<p>Here's a rough sequence diagram demonstrating the notification flow:</p>\n<pre class=\"mermaid\"><code>sequenceDiagram\n  participant External\n  participant googleMail as Google Mail Routing\n  participant googleGroup as Security Inbox&lt;br/&gt;(Google Group)\n  participant pypiAdmins as PyPI Admins\n\n  External-&gt;&gt;googleMail: sends an email report\n  googleMail-&gt;&gt;googleGroup: sends an email report\n  googleGroup-&gt;&gt;+pypiAdmins: sends an email report\n  pypiAdmins--&gt;&gt;+External: Hi, thanks for your report...\n  pypiAdmins--&gt;&gt;-googleGroup: CC response to group\n\ntitle Previous Inbound Flow</code></pre>\n<p>I wanted to answer a couple of questions, so as to have some data to work with:</p>\n<ol>\n<li>How many inbound malware reports do we receive? (daily/weekly/monthly)</li>\n<li>How long does it take for a response from an administrator to remove the reported malware?</li>\n</ol>\n<p>Answering these questions with the email-based system will not be 100% accurate,\nas there are some conditions that lead to inaccuracies,\nbut since we're looking at large volumes of records,\nit's unlikely that the inaccuracies will lead to material differences in the numbers.\nSome examples:</p>\n<ul>\n<li>A reporter submits multiple reports on a single message</li>\n<li>A reporter replies to their original email for a new report,\n  instead of starting a new thread/conversation</li>\n<li>Folks contacting admins for support may email the Security Inbox</li>\n<li>Security-related issues that are not malware reports</li>\n<li>Spam</li>\n</ul>\n<p>This was largely to establish a baseline of where we are today.\nWith this measurement, we can then observe whether changes to our process\nhave a positive or negative impact on the volume and response times.</p>\n<p><em>Note</em>: This analysis is not an accurate measurement of distinct malware packages reported,\nas multiple researchers may report the same package,\nwhich will show up as distinct conversation threads.\nWe could try to normalize the packages, however since the emails are unstructured,\nthat may take more effort than is worthwhile.\nIn any case, admins respond to duplicates, so there's still non-zero effort being done.</p>\n<p>Earlier this year one of our admins posted some removals stats on Twitter (before we had a blog!):</p>\n<blockquote class=\"twitter-tweet\"><p lang=\"en\" dir=\"ltr\">in 2022, the <a href=\"https://twitter.com/pypi?ref_src=twsrc%5Etfw\">@pypi</a> team removed &gt;12,000 unique projects. each were instances of spam, typosquatting, dependency confusion, exfiltration and/or malware.<br><br>2022: ~12K (mostly malware)<br>2021: ~27K (mostly dep confusion)<br>2020: ~500<br>2019: 65<br>2018: 137<br>2017: 38</p>&mdash; Dustin Ingram (@di_codes) <a href=\"https://twitter.com/di_codes/status/1610781657128108033?ref_src=twsrc%5Etfw\">January 4, 2023</a></blockquote>\n<script async src=\"https://platform.twitter.com/widgets.js\" charset=\"utf-8\"></script>\n\n<h2 id=\"what-is-the-frequency-of-inbound-malware-reports\">What is the frequency of inbound malware reports?</h2>\n<p>To determine this answer, I elected to use the emails themselves,\nsince that's the feed we have of details.\nWhile this is an imperfect data source, it should be good enough to make some early assertions.\nWe can also leverage any statistics generated thus far to assist in measuring future progress.</p>\n<p>My PyPI Google Workspace account was created on 2023-03-02 and began receiving <a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#115;&#101;&#99;&#117;&#114;&#105;&#116;&#121;&#64;&#112;&#121;&#112;&#105;&#46;&#111;&#114;&#103;\">&#115;&#101;&#99;&#117;&#114;&#105;&#116;&#121;&#64;&#112;&#121;&#112;&#105;&#46;&#111;&#114;&#103;</a> emails after that.\nGoogle Groups does not surface any APIs I could find\nthat allows an authenticated user to list/read conversations directly from the group.</p>\n<p>This approach provides a cleaner signal-to-noise ratio,\nas I often delete the random emails that are sent to the Security Inbox if they are not useful\n(spam, marketing, etc).\nWe can either accept this approach of data collection for the past few months,\nor pursue other methods for collecting longer-term data from older accounts/mailing lists,\nall subject to different data quality issues.</p>\n<p>Using the data based on 1,303 email threads sent to the Security Inbox\nby 2023-08-14, we can produce this chart:</p>\n<p><img alt=\"Inbound Malware Reports by Date\" src=\"../../assets/2023-09-18-inbound-malware-reporting/inbound-by-date.png\" /></p>\n<p>The same data, grouped by week number:</p>\n<p><img alt=\"Inbound Malware Reports by Week\" src=\"../../assets/2023-09-18-inbound-malware-reporting/inbound-by-week.png\" /></p>\n<p>One observation is that post-<a href=\"https://status.python.org/incidents/qy2t9mjjcc7g\">PyPI Weekend Suspension</a>\nin May (Week 20), the overall volume drops for a while.\nThere's no hard evidence as to why, but it's interesting\nthat a brief disruption reduced some of the toil maintainers currently handle.</p>\n<p>Form completeness, here's the monthly view:</p>\n<p><img alt=\"Inbound Malware Reports by Month\" src=\"../../assets/2023-09-18-inbound-malware-reporting/inbound-by-month.png\" /></p>\n<hr />\n<h2 id=\"how-long-does-it-take-to-respond\">How long does it take to respond?</h2>\n<p>Why is response time interesting?\nThe longer a malicious package is available for end users to install,\nthe more people and systems it may affect.\nThis is further complicated to any package mirrors that capture the malware,\nand may not remove it as quickly as PyPI admins.\nWe have received anecdotal evidence from reporters that PyPI admins are\nalready quite fast at handling inbound reports (<code>#humblebrag</code>),\nbut let's see if we can get data out of the same emails.</p>\n<p>Again, since the nature of email isn't 100% accurate in this case,\nwe'll rely on calculating the duration of time (in minutes)\nbetween the first message of a thread and the last message of a thread.\nThis doesn't account for the occasional behavior of a reporter\nre-using the same thread to report more packages,\nnor does it reflect any other back-and-forth communication\nbetween admins and reporters.\nAs such, removing any threads that have more than 4 total messages\nhelps remove outliers from the analysis.</p>\n<p>Inbound reports come in at any time of day,\nand can also be automatically generated by reporters.\nIt's common for reports to wait to be handled while we\u2019re asleep.\nWeekends and holidays often have longer response times as well.</p>\n<p>On occasion an inbound report may get overlooked,\nsomething we're trying to solve with a new system, more on this later.</p>\n<p>A higher response time may indicate that the mostly-volunteer admins\nmissed responding to it the first time around.\nFor the purpose of this analysis, I've excluded 7 total response times\nthat exceed 14 days (20,160 minutes) to remove those outliers.</p>\n<p><img alt=\"Response Times by Date\" src=\"../../assets/2023-09-18-inbound-malware-reporting/med-response-times-by-date.png\" /></p>\n<p>Using median values over averages\nhelps us account for outliers at either end of the spectrum.</p>\n<p>Applying a linear trend line to the collected data shows\nthat response times are generally decreasing over time, which is a good thing.</p>\n<p>Here's a distribution of response times for the data collected since March 2023:</p>\n<p><img alt=\"Response Times Histogram\" src=\"../../assets/2023-09-18-inbound-malware-reporting/hist-resp-time-minutes.png\" /></p>\n<p>This chart informs us that most responses to reports are completed\nin under ~485 minutes (~8 hours),\nand almost all are done within a few days of the report, with a long tail.</p>\n<p>All said, that doesn't capture the full picture of response times,\nwhich is why we've been working on a new system to help us respond faster,\nand produce better reports.</p>\n<h2 id=\"whats-changed-as-a-result\">What's changed as a result?</h2>\n<p>Part of the PyPI Malware Reporting and Response project is\nto explore ways to decrease the response times even further,\nwhile reducing the toil on maintainers, and increasing visibility to reporters.</p>\n<p>As a result of this analysis, one change we've made so far is to leverage\na shared inbox system <a href=\"https://www.helpscout.com/\">Help Scout</a>\nto receive inbound emails and allow us to tag, assign, and close out reports.\nThis helps is by not missing reports, and preventing duplicate responses from admins.</p>\n<p>Here's how we updated our Google Workspace flow so we can continue\nto receive inbound emails, as well as copy the conversations from Help Scout\nfor long-term archival into Google Groups.</p>\n<pre class=\"mermaid\"><code>sequenceDiagram\n  participant External\n  participant googleMail as Google Mail Routing\n  participant googleGroup as Google Group&lt;br/&gt;Archive\n  participant helpScout as Security Inbox&lt;br/&gt;(Help Scout)\n  participant pypiAdmins as PyPI Admins\n\n  External-&gt;&gt;googleMail: sends an email report\n  googleMail-&gt;&gt;+helpScout: sends an email report\n  googleMail-&gt;&gt;googleGroup: archive email\n  helpScout-&gt;&gt;+pypiAdmins: inbound notification\n  pypiAdmins--&gt;&gt;-helpScout: Hi, thanks for your report...\n  helpScout-&gt;&gt;googleGroup: archive via auto-bcc\n  helpScout--&gt;&gt;-External: Hi, thanks for your report...\n\ntitle Updated Inbound Flow</code></pre>\n<p>There's no change to end users, and we hope our change keeps us on track\nto continue to respond to reports in a timely fashion.</p>\n<p>Here's an initial look at the response times since we started using Help Scout,\n(2023-09-05) using their reporting.\nWith a total of 31 conversations in the time period since we started using Help Scout,\nand comparing to the final two weeks of data from the previous chart (59 conversations),\nwe can see that the response times have improved:</p>\n<table>\n<thead>\n<tr>\n<th>Response time bucket</th>\n<th>% of total</th>\n<th>pre-Help Scout</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>&lt; 15 min</td>\n<td>40%</td>\n<td>53%</td>\n</tr>\n<tr>\n<td>15-30 min</td>\n<td>20%</td>\n<td>10%</td>\n</tr>\n<tr>\n<td>30-60 min</td>\n<td>20%</td>\n<td>5%</td>\n</tr>\n<tr>\n<td>1-2 hours</td>\n<td>10%</td>\n<td>7%</td>\n</tr>\n<tr>\n<td>2-12 hours</td>\n<td>10%</td>\n<td>15%</td>\n</tr>\n<tr>\n<td>12+ hours</td>\n<td>0%</td>\n<td>10%</td>\n</tr>\n</tbody>\n</table>\n<p>We can now happily report that 80% all reports are responded to\n<strong>within 60 minutes of receipt</strong>,\nwith 100% are responded to within 12 hours.</p>\n<p>We will continue to monitor our response times and volumes,\nand make adjustments as needed.</p>\n<h2 id=\"whats-next\">What's next?</h2>\n<p>We're working on designing a new system to help us respond faster,\nbased in inbound reports, and provide better outcomes.\nIt's still very early, and we're incorporating a lot of ideas in the design\nbased on collective experience of PyPI admins, external researchers and reporters.</p>\n<p>We invite you to engage in the conversation on a more machine-readable\nformat for reporting malware in\n<a href=\"https://github.com/pypi/warehouse/issues/14503\">this GitHub Issue</a>,\nand consider sending pull requests where appropriate.</p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-09-18-inbound-malware-reporting.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-09-18T06:09:00+00:00", "authors": [{"name": "Mike Fiedler"}], "tags": ["security", "transparency"]}, {"id": "https://blog.pypi.org/posts/2023-11-14-1-pypi-completes-first-security-audit/", "url": "https://blog.pypi.org/posts/2023-11-14-1-pypi-completes-first-security-audit/", "title": "PyPI has completed its first security audit", "content_html": "<p><em>This is part one in a three-part series. See <a href=\"../2023-11-14-2-security-audit-remediation-warehouse/\">part two here</a>, and <a href=\"../2023-11-14-3-security-audit-remediation-cabotage/\">part three here</a></em></p>\n<p>We are proud to announce that PyPI has completed its first ever external security audit.\nThis work was funded in partnership with the <a href=\"https://www.opentech.fund/\">Open Technology Fund</a> (OTF), <a href=\"https://www.opentech.fund/results/supported-projects/pypi-improvements/\">a previous supporter</a> of security-related improvements to PyPI.</p>\n<!-- more -->\n\n<p>The Open Technology Fund selected <a href=\"https://www.trailofbits.com/\">Trail of Bits</a>, an industry-leading cybersecurity firm with significant open-source and Python experience, to perform the audit.\nTrail 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.</p>\n<h2 id=\"scope\">Scope</h2>\n<p>The audit was focused on \"Warehouse\", the open-source codebase that powers <a href=\"https://pypi.org\">https://pypi.org</a>, and on \"cabotage\", the custom open-source container orchestration framework we use to deploy Warehouse.\nIt included code review of both codebases, prioritizing areas that accept user input, provide APIs and other public surfaces.\nThe audit also covered the continuous integration / continuous deployment (CI/CD) configurations for both codebases.</p>\n<h2 id=\"findings\">Findings</h2>\n<p>Overall, the auditors determined the Warehouse codebase \"was adequately tested and conformed to widely accepted best practices for secure Python and web development,\" and that while the cabotage codebase lacks the same level of testing, they did not identify any high severity issues in either codebase.</p>\n<h2 id=\"results-impact\">Results &amp; Impact</h2>\n<p>As a result of the audit, Trail of Bits detailed 29 different advisories discovered across both codebases.\nWhen evaluating severity level of each advisory, 14 were categorized as \"informational\", 6 as \"low\", 8 as \"medium\" and zero as \"high\".\nAt 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.</p>\n<h2 id=\"more-details\">More details</h2>\n<p>In the interest of transparency, today we are publishing the <a href=\"https://github.com/trailofbits/publications#technology-product-reviews\">full results of the audit</a>, as prepared by Trail of Bits.\nYou can read more about the audit from their perspective in their <a href=\"https://blog.trailofbits.com/2023/11/14/our-audit-of-pypi/\">accompanying blog post</a>.</p>\n<p>Additionally, in two additional blog posts published today, Mike Fiedler (PyPI Security &amp; Safety Engineer) goes into detail about <a href=\"../2023-11-14-2-security-audit-remediation-warehouse/\">how we remediated these findings in Warehouse</a> and Ee Durbin (Python Software Foundation Director of Infrastructure) <a href=\"../2023-11-14-3-security-audit-remediation-cabotage/\">similarly details remediation's in cabotage</a>.</p>\n<h2 id=\"acknowledgements\">Acknowledgements</h2>\n<p>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.\nWe would also like to thank Trail of Bits for being a dependable, thorough and thoughtful partner throughout the process.</p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-11-14-1-pypi-completes-first-security-audit.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-11-14T00:00:00+00:00", "authors": [{"name": "Dustin Ingram"}], "tags": ["security", "transparency"]}, {"id": "https://blog.pypi.org/posts/2023-11-14-2-security-audit-remediation-warehouse/", "url": "https://blog.pypi.org/posts/2023-11-14-2-security-audit-remediation-warehouse/", "title": "Security Audit Remediation: Warehouse", "content_html": "<p><em>This is part two in a three-part series. See <a href=\"../2023-11-14-1-pypi-completes-first-security-audit/\">part one here</a>, and <a href=\"../2023-11-14-3-security-audit-remediation-cabotage/\">part three here</a></em>.</p>\n<p>This post is a deeper dive into the remediation of the security audit findings\nfor the Warehouse - the main codebase for <a href=\"https://pypi.org\">PyPI.org</a>.</p>\n<p>The audit report can be found <a href=\"../2023-11-14-1-pypi-completes-first-security-audit/\">here</a>.\nI highly recommend reading that for the fullest context first.</p>\n<!-- more -->\n\n<h2 id=\"findings\">Findings</h2>\n<p>The audit report identified 18 findings for Warehouse,\nalong with some code quality suggestions.\nThis post will focus on the findings and their remediation.\nSome of the code quality suggestions were implemented, others deferred.</p>\n<p>Here's a table of the items that are relevant to warehouse, and their status:</p>\n<table>\n<thead>\n<tr>\n<th>ID</th>\n<th>Title</th>\n<th>Severity</th>\n<th>Difficulty</th>\n<th>Status</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>TOB-PYPI-1</td>\n<td>Unsafe input handling in \"Combine PRs\" workflow</td>\n<td>Informational</td>\n<td>High</td>\n<td><a href=\"https://github.com/pypi/warehouse/pull/14528\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-2</td>\n<td>Weak signatures used in AWS SNS verification</td>\n<td>Medium</td>\n<td>Undetermined</td>\n<td><a href=\"https://github.com/pypi/warehouse/pull/14387\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-4</td>\n<td>Lack of rate limiting on endpoints that send email</td>\n<td>Low</td>\n<td>High</td>\n<td>Accepted</td>\n</tr>\n<tr>\n<td>TOB-PYPI-5</td>\n<td>Account status information leak for frozen and disabled accounts</td>\n<td>Medium</td>\n<td>Low</td>\n<td><a href=\"https://github.com/pypi/warehouse/pull/14449\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-6</td>\n<td>Potential race conditions in search locking</td>\n<td>Low</td>\n<td>High</td>\n<td><a href=\"https://github.com/pypi/warehouse/pull/14640\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-7</td>\n<td>Use of multiple distinct URL parsers</td>\n<td>Informational</td>\n<td>Undetermined</td>\n<td><a href=\"https://github.com/pypi/warehouse/pull/14497\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-8</td>\n<td>Overly permissive CSP headers on XML views</td>\n<td>Informational</td>\n<td>High</td>\n<td><a href=\"https://github.com/pypi/warehouse/pull/14452\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-9</td>\n<td>Missing Permissions-Policy</td>\n<td>Medium</td>\n<td>High</td>\n<td><a href=\"https://github.com/pypi/infra/pull/160\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-10</td>\n<td>Domain separation in file digests</td>\n<td>Low</td>\n<td>Low</td>\n<td><a href=\"https://github.com/pypi/warehouse/pull/14492\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-11</td>\n<td>Object storage susceptible to TOC/TOU due to temporary files</td>\n<td>Informational</td>\n<td>High</td>\n<td>Accepted</td>\n</tr>\n<tr>\n<td>TOB-PYPI-12</td>\n<td>HTTP header is silently trusted if token mismatches</td>\n<td>Informational</td>\n<td>High</td>\n<td><a href=\"https://github.com/pypi/warehouse/pull/14499\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-13</td>\n<td>Bleach library is deprecated</td>\n<td>Informational</td>\n<td>Undetermined</td>\n<td><a href=\"https://github.com/pypi/warehouse/pull/14526\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-14</td>\n<td>Weak hashing in storage backends</td>\n<td>Medium</td>\n<td>High</td>\n<td>Accepted</td>\n</tr>\n<tr>\n<td>TOB-PYPI-15</td>\n<td>Uncaught exception with crafted README</td>\n<td>Informational</td>\n<td>Medium</td>\n<td>Accepted</td>\n</tr>\n<tr>\n<td>TOB-PYPI-16</td>\n<td>ReDoS via zxcvbn-python dependency</td>\n<td>Informational</td>\n<td>High</td>\n<td>Accepted</td>\n</tr>\n<tr>\n<td>TOB-PYPI-23</td>\n<td>Insecure XML processing in XMLRPC server</td>\n<td>Low</td>\n<td>Low</td>\n<td><a href=\"https://github.com/pypi/warehouse/pull/14491\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-27</td>\n<td>Denial-of-service risk on tar.gz uploads</td>\n<td>Informational</td>\n<td>Medium</td>\n<td>Accepted</td>\n</tr>\n<tr>\n<td>TOB-PYPI-29</td>\n<td>Unescaped values in LIKE SQL queries</td>\n<td>Informational</td>\n<td>Low</td>\n<td>Accepted</td>\n</tr>\n</tbody>\n</table>\n<p><em>IDs are non-consecutive, as the audit report included findings for cabotage as well.</em></p>\n<p>For some of the Remediated entries and all the Accepted ones, I'll go into more detail below.</p>\n<h2 id=\"details\">Details</h2>\n<p>Now that you've had a chance to read the original audit report,\nand can see that we've remediated most of the findings,\nI wanted to take some time to dig into some specifics of particular findings.</p>\n<h3 id=\"tob-pypi-2-weak-signatures-used-in-aws-sns-verification\">TOB-PYPI-2: Weak signatures used in AWS SNS verification</h3>\n<p>PyPI uses AWS SES to send emails to users.\nThe SES configuration is set to use a\n<a href=\"https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#mailing-list-notification\">Message Delivery Status</a>\ntopic, which sends a notification to an AWS SNS topic,\nwhich then sends a notification to our application.</p>\n<p>This is useful for things like \"Accepted/Delivered\",\nbut more importantly \"Bounced\" and \"Complaint\" notifications,\nwhich change the status of user accounts.\nWe don't want to send more emails to a known bad address,\nand we don't want to send emails to users who have marked us as spam.</p>\n<p>Since PyPI receives a webhook from AWS SNS,\nit needs to verify the signature of the message.</p>\n<p>Verifying inbound SNS messages has generally been left up to the user.\n<a href=\"https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html\">The AWS SNS docs</a>\nare clear about that.</p>\n<p>We had previously implemented signature verification for version 1,\nwhich uses the SHA1 hash algorithm,\nas that is what existed when we implemented it.</p>\n<p>As time evolved, and AWS SNS added support for SHA256,\nthe path to upgrade was still left in the hands of the user.\nSNS still defaults to SHA1 (<code>SignatureVersion: '1'</code>),\nand there's no Python SDK function to call to validate the signature for you.</p>\n<p>This is <a href=\"https://github.com/boto/boto3/issues/2508\">also an outstanding request from boto3 users</a>.</p>\n<p>In September 2022, AWS SNS added support for SHA256 signatures,\nand shared the details in <a href=\"https://aws.amazon.com/blogs/security/sign-amazon-sns-messages-with-sha256-hashing-for-http-subscriptions/\">this blog post</a>.\nThey also added support for verification in some of the client-side SDKs,\nbut sadly Python is not one of them yet.</p>\n<p>While we were already validating SignatureVersion 1,\nwe took this opportunity to add support for SignatureVersion 2,\nupdate our settings, and now only accept SHA256 signatures.</p>\n<p>As an <a href=\"https://aws.amazon.com/developer/community/heroes/\">AWS Hero</a>,\nI reached out to Farrah Campbell who heads up Modern Compute Community at AWS,\nand she quickly connected me with the AWS SNS service team for a chat.\nWe discussed some of the challenges, as well as some ideas for the path forward.</p>\n<p>I'm hopeful that sometime in the future we will see two big things:</p>\n<ul>\n<li>message validation in <code>boto3</code> for both signature versions\n  This would enable us to remove <a href=\"https://github.com/pypi/warehouse/blob/256d3e374ff8c2c3b29ac1b3040c88ddfb7e1d76/warehouse/utils/sns.py\"><code>MessageVerifier</code> we added to warehouse</a>, and benefit from any future enhancements to the validation process.</li>\n<li>update AWS SNS to default to <code>SignatureVersion: 2</code> (SHA256).\n  This could be a breaking change for users who have not updated their settings,\n  but would be a good step forward for security.\n  This make take some time, as the new signature version was only added a year ago.\n  I'll leave that up to the SNS service team.</li>\n</ul>\n<h3 id=\"tob-pypi-4-lack-of-rate-limiting-on-endpoints-that-send-email\">TOB-PYPI-4: Lack of rate limiting on endpoints that send email</h3>\n<p>We accepted this finding, as we need to send emails to unverified users\nas part of the account creation process, and we don't want to block that.</p>\n<p>The finding details that PyPI doesn't apply blanket rate limiting, which is correct.\nThe endpoints that send emails to <strong>unverified</strong> addresses are protected via rate limiting.</p>\n<p>PyPI has compensating controls in place to prevent abuse,\nsuch as preventing too many password reset emails from being sent to a single user.</p>\n<p>Since the risk here was to the cost and reputation of the email service,\nwe decided to accept this finding.\nAt some future point we may revisit the rate limiting strategy for sending email.</p>\n<h3 id=\"tob-pypi-6-potential-race-conditions-in-search-locking\">TOB-PYPI-6: Potential race conditions in search locking</h3>\n<p>This is another case of \"we implemented something that was good at the time,\nand as time went on a better solution became available\".</p>\n<p>We had written a context manager to handle locking the search index\nwhen performing updates, to prevent multiple processes from trying to update\nthe search index at the same time.\nThe implementation wasn't tied to the underlying Redis lock expiration,\nso could lead to the Redis-lock expiring, but Python believing it was still locked.</p>\n<p>Here we updated our implementation to use a context manager that <code>redis-py</code>\nnow provides, instead of crafting our own.</p>\n<p>A solid reminder to check back on your libraries and services now and then,\nto see if there's new features that can help you out.</p>\n<h3 id=\"tob-pypi-11-object-storage-susceptible-to-toctou-due-to-temporary-files\">TOB-PYPI-11: Object storage susceptible to TOC/TOU due to temporary files</h3>\n<p>This is a complex timing attack, which requires a level of access to the system\nthat would allow for a more direct attack.\nThe finding itself details that if an attacker could execute this,\nthey are more likely to do other kinds of damage.</p>\n<p>The complexity of navigating between our various storage backends/client APIs\ndoes not appear to be worth the resulting defense in depth,\ngiven the required access level to exploit.</p>\n<p>We have a <a href=\"https://github.com/pypi/warehouse/pull/14568\">draft PR</a>\nwith a start of implementation should we decide to pursue this.</p>\n<h3 id=\"tob-pypi-14-weak-hashing-in-storage-backends\">TOB-PYPI-14: Weak hashing in storage backends</h3>\n<p>This is specifically about the Backblaze B2 storage backend,\none of PyPI's current object storage providers,\nwhich does not currently support SHA-256 checksums.\nThey do support SHA-1 which is useful for detecting data corruption in transit,\nbut is insufficient for non-colliding checksums - we have to use MD5 for that.</p>\n<p>During the audit, we reached out to the Backblaze team to discuss and determined it's on their roadmap, and when they implement it, we'll update our usage accordingly.</p>\n<h3 id=\"tob-pypi-15-uncaught-exception-with-crafted-readme\">TOB-PYPI-15: Uncaught exception with crafted README</h3>\n<p>This finding discovered a bug in <code>docutils</code>, which PyPI uses via the <code>readme_renderer</code> library to render project descriptions from reStructuredText and Markdown to HTML.</p>\n<p>The bug is <a href=\"https://sourceforge.net/p/docutils/bugs/474/\">tracked here</a>,\nand has yet to see a response from the maintainers.</p>\n<p>It only applies to client-side behavior when using reStructuredText for a README,\nso we've accepted this finding.\nAdditionally, any user performing <code>twine check</code> prior to upload\nwill surface this issue.</p>\n<p>Once the bug is fixed, we'll update!</p>\n<h3 id=\"tob-pypi-16-redos-via-zxcvbn-python-dependency\">TOB-PYPI-16: ReDoS via <code>zxcvbn-python</code> dependency</h3>\n<p>Direct from the audit:</p>\n<blockquote>\n<p>This finding is purely informational.\nWe believe that it has virtually no impact, like many ReDoS vulnerabilities,\ndue to Warehouse\u2019s deployment architecture.</p>\n</blockquote>\n<p>Enough said.</p>\n<h3 id=\"tob-pypi-23-insecure-xml-processing-in-xmlrpc-server\">TOB-PYPI-23: Insecure XML processing in XMLRPC server</h3>\n<p>The audit began when the Warehouse deployment was on Debian 11 <code>bullseye</code>,\nand as part of normal maintenance we upgraded to\n<a href=\"https://github.com/pypi/warehouse/pull/14491\">Debian 12 <code>bookworm</code></a>\nwhile the audit was in progress.</p>\n<p>Python XMLRPC uses <a href=\"https://libexpat.github.io/\"><code>expat</code></a> for XML parsing.\nThe version of <code>expat</code> in Debian <code>bullseye</code> was <code>2.2.10</code>, which was vulnerable\nto the specific attack detailed in the audit report.</p>\n<p>With <code>bookworm</code>, the version of <a href=\"https://packages.debian.org/bookworm/libexpat1\"><code>expat</code> is <code>2.5.0</code></a>,\nwhich is not vulnerable. (Generally considered <a href=\"https://github.com/python/cpython/pull/26945\">fixed as of 2.4.1.</a>)</p>\n<p>This was a tricky one to track down, as once the report came in\nI was unable to reproduce the issue locally, as I had already upgraded.</p>\n<p>Using some <code>git bisect</code> magic, I was able to track down the exact commit\nthat fixed the issue (the <code>bookworm</code> upgrade),\nand then it was a matter of figuring out which library had changed.</p>\n<p>After figuring it out, I worked with the auditors to update their\nrecommendations to reflect the upgrade.\nUntil now, the general recommendation was to adopt <code>defusedxml</code>,\nwhich might have proven harder as we delegate the majority of our XML parsing\nto <code>pyramid-rpc</code>, which uses <code>xmlrpc.client</code> from the standard library.</p>\n<p>If you want to check your own installation, you can run the following:</p>\n<pre class=\"highlight\"><code class=\"language-bash\">python -c \"import pyexpat; print(pyexpat.EXPAT_VERSION)\"</code></pre>\n<p>On <code>bookworm</code>, we get <code>expat_2.5.0</code>, which is not affected by the vulnerability.</p>\n<p>This was remediated by underlying OS update to <code>bookworm</code>.\nDebian distributions pin to a specific version of libraries\nfor the duration of that distribution version.</p>\n<h3 id=\"tob-pypi-27-denial-of-service-risk-on-targz-uploads\">TOB-PYPI-27: Denial-of-service risk on tar.gz uploads</h3>\n<p>This is a tricky one, as it's a tradeoff between usability and security.</p>\n<p>The audit report details a specific attack vector,\nwhere a malicious user could upload a tarball with a highly-compressed file,\nwhich would cause the server to spend a lot of time decompressing it.</p>\n<p>Since we accept uploads from the general public,\nwe have to take precautions whenever possible to prevent abuse.\nWhen it comes to ZIP files (which all <code>.whl</code> or \"wheel\" files are), we already have a mechanism to detect\ndecompression bombs, and reject them.</p>\n<p>However, since <code>.tar.gz</code> files do not advertise file sizes as metadata,\nin order to detect a decompression bomb we would have to decompress the entire file anyhow.</p>\n<p>As the report notes, our deployment architecture compensates for this behavior,\nwhere we have a dedicated worker pool for handling uploads.</p>\n<p>We may apply additional restrictions at the system level in the future,\nbut for now we've accepted this finding.</p>\n<h3 id=\"tob-pypi-29-unescaped-values-in-like-sql-queries\">TOB-PYPI-29: Unescaped values in <code>LIKE SQL</code> queries</h3>\n<p>The risk here is that a query could \"walk the table\"\nand not take advantage of any indexes, leading to higher resource usage.</p>\n<p>The majority of the places where we use unescaped <code>LIKE</code> queries\nis in PyPI admin-only interface, where want to allow admins to search for users, packages, etc.</p>\n<p>For the one place where we allow public-facing <code>LIKE</code> queries,\nthere are already rate limits in place to prevent abuse.\nThe table in question is also smaller than 1M rows, so walking an un-indexed column\nwould not be a significant resource usage, and takes a handful of extra milliseconds.</p>\n<p>The potential higher resource usage would be limited to malicious internal actors,\nand if we can't trust each other, we've got bigger problems to deal with.</p>\n<p>We've accepted this finding, and will continue to monitor all of relevant resources.</p>\n<h2 id=\"summary\">Summary</h2>\n<p>Working with the folks at <a href=\"https://www.trailofbits.com/\">Trail of Bits</a> was a pleasure,\nand I'm thankful for their thoroughness and professionalism.</p>\n<p>While the audit was funded through the <a href=\"https://opentech.fund/\">Open Technology Fund</a>,\nmy work on remediation would not have been as timely\nif not funded by <a href=\"https://aws.amazon.com/\">Amazon Web Services</a>\nto work as the <a href=\"../2023-05-09-announcing-pypi-safety-and-security-engr-role/\">PyPI Safety and Security Engineer</a>.\nI am grateful for the continued support of both organizations\nin making PyPI a safer place for all Python users.</p>\n<p><em>Mike Fiedler is the inaugural PyPI Safety &amp; Security Engineer.</em></p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-11-14-2-security-audit-remediation-warehouse.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-11-14T00:00:01+00:00", "authors": [{"name": "Mike Fiedler"}], "tags": ["security", "transparency"]}, {"id": "https://blog.pypi.org/posts/2023-11-14-3-security-audit-remediation-cabotage/", "url": "https://blog.pypi.org/posts/2023-11-14-3-security-audit-remediation-cabotage/", "title": "Security Audit Remediation: cabotage", "content_html": "<p><em>This is part three in a three-part series.\nSee <a href=\"../2023-11-14-1-pypi-completes-first-security-audit/\">part one here</a>,\nand <a href=\"../2023-11-14-2-security-audit-remediation-warehouse/\">part two here</a></em>.</p>\n<p>This post is a deeper dive into the remediation of the security audit findings\nfor cabotage - the <a href=\"https://github.com/cabotage/cabotage-app\">codebase</a> that\ndeploys <a href=\"https://pypi.org\">PyPI</a> and its supporting services such as\n<a href=\"https://github.com/pypi/conveyor\">conveyor</a>,\n<a href=\"https://github.com/pypi/camo\">camo</a>,\nand <a href=\"https://github.com/pypi/inspector\">inspector</a>.</p>\n<!-- more -->\n\n<p>Relative to the <a href=\"https://github.com/pypi/warehouse\">warehouse codebase</a> that\n<em>is</em> <a href=\"https://pypi.org\">PyPI</a>, cabotage is not as widely known.\nThe goals of cabotage are to provide a seamless and secure way of deploying\narbitrary services into a <a href=\"https://kubernetes.io\">Kubernetes</a> cluster\nin a \"<a href=\"https://12factor.net\">Twelve-Factor</a>\" style.\nThere are also a number of firm opinions baked into cabotage that provide\nend-to-end TLS, protection against recovering secrets through the web UI,\nand isolation between tenants inside the cluster.</p>\n<p>cabotage was initially developed in 2018 as part of the\n<a href=\"https://pyfound.blogspot.com/2017/11/the-psf-awarded-moss-grant-pypi.html\">Mozilla Open Source Support Award</a>\nthat enabled the <a href=\"https://python.org/psf/\">Python Software Foundation</a> (PSF)\nto fund a team of contracted developers and a project manager to\ncomplete the development and deployment of warehouse and sunset the\n<a href=\"https://github.com/pypi/legacy\">original PyPI codebase</a>.</p>\n<p>A primary goal of cabotage is to reduce the PSF Infrastructure's\ndependence on a specific provider for running PyPI,\nwhile providing self-service of configuration for project administrators\nand fully automated deployments.\nIt is in-effect a \"Platform as a Service\" that deploys applications\ninto bog-standard Kubernetes clusters, no YAML required.</p>\n<p>To date, cabotage has deployed 3,901 releases to PyPI since 2018,\nand 7,377 releases in total across its current services \"fleet\".</p>\n<p>The audit report can be found <a href=\"../2023-11-14-1-pypi-completes-first-security-audit/\">here</a>.\nReading that <em>before</em> you dive in will provide the fullest context.</p>\n<h2 id=\"findings\">Findings</h2>\n<p>Eleven findings resulted from the audit along with twelve code quality\nsuggestions.\nThis post will focus on the findings and their remediation.\nSome of the code quality suggestions were implemented, others deferred.</p>\n<p>Here's a table of the items that are relevant to cabotage, and their status:</p>\n<table>\n<thead>\n<tr>\n<th>ID</th>\n<th>Title</th>\n<th>Severity</th>\n<th>Difficulty</th>\n<th>Status</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>TOB-PYPI-3</td>\n<td>Vulnerable dependencies in cabotage</td>\n<td>Undetermined</td>\n<td>Low</td>\n<td><a href=\"https://github.com/cabotage/cabotage-app/compare/ad532ea0dcb7c3dd5228ee8b1312ddad9c70c6af...3e045eea677dd4af71239aad682c4b8ab3a39d3a\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-17</td>\n<td>Use of shell=True in subprocesses</td>\n<td>Medium</td>\n<td>Medium</td>\n<td><a href=\"https://github.com/cabotage/cabotage-app/pull/36\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-18</td>\n<td>Use of HMAC with SHA1 for GitHub webhook payload validation</td>\n<td>Low</td>\n<td>High</td>\n<td><a href=\"https://github.com/cabotage/cabotage-app/pull/37\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-19</td>\n<td>Potential container image manipulation through malicious Procfile</td>\n<td>Medium</td>\n<td>High</td>\n<td><a href=\"https://github.com/cabotage/cabotage-app/pull/39\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-20</td>\n<td>Repository confusion during image building</td>\n<td>Medium</td>\n<td>Medium</td>\n<td><a href=\"https://github.com/cabotage/cabotage-app/pull/46\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-21</td>\n<td>Brittle X.509 certificate rewriting</td>\n<td>Informational</td>\n<td>Undetermined</td>\n<td><a href=\"https://github.com/cabotage/cabotage-app/pull/38\">Accepted</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-22</td>\n<td>Unused dependencies in cabotage</td>\n<td>Informational</td>\n<td>Undetermined</td>\n<td><a href=\"https://github.com/cabotage/cabotage-app/pull/35\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-24</td>\n<td>Missing resource integrity check of third-party resources</td>\n<td>Informational</td>\n<td>High</td>\n<td><a href=\"https://github.com/cabotage/cabotage-app/pull/40\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-25</td>\n<td>Brittle secret filtering in logs</td>\n<td>Medium</td>\n<td>Low</td>\n<td><a href=\"https://github.com/cabotage/cabotage-app/pull/47\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-26</td>\n<td>Routes missing access controls</td>\n<td>Low</td>\n<td>High</td>\n<td><a href=\"https://github.com/cabotage/cabotage-app/pull/41\">Remediated</a></td>\n</tr>\n<tr>\n<td>TOB-PYPI-28</td>\n<td>Deployment hook susceptible to race condition due to temp files</td>\n<td>Informational</td>\n<td>High</td>\n<td>Remediated <a href=\"https://github.com/cabotage/cabotage-app/pull/45\">1</a>, <a href=\"https://github.com/cabotage/cabotage-app/pull/42\">2</a></td>\n</tr>\n</tbody>\n</table>\n<p><em>IDs are non-consecutive, as the audit report included findings for Warehouse as well.</em></p>\n<h2 id=\"details\">Details</h2>\n<h3 id=\"tob-pypi-3-vulnerable-dependencies-in-cabotage\">TOB-PYPI-3:  Vulnerable dependencies in cabotage</h3>\n<p>The maintenance of cabotage has been primarily driven by the need for new\nfeatures or to mitigate issues raised. As a result dependency management and\nupgrades have often been done as a byproduct of other changes.</p>\n<p>During review, there were a number of dependencies with known vulnerabilities\nfound. Of the nine vulnerabilities noted, only\n<a href=\"https://github.com/advisories/GHSA-cg8c-gc2j-2wf7\">GHSA-cg8c-gc2j-2wf7</a>\nwas determined impact cabotage and was remediated by migrating to the latest\nrelease of the maintained fork of flask-security, flask-security-too\n(<a href=\"https://github.com/cabotage/cabotage-app/compare/3f3e2ba7753f8e4434b562d226118a0dfe873498...3e045eea677dd4af71239aad682c4b8ab3a39d3a\">diff</a>).</p>\n<p>In order to avoid falling behind in this kind of maintenance, automated\ndependency management was added along with updates to all of known vulnerable\ndependencies\n(<a href=\"https://github.com/cabotage/cabotage-app/compare/ad532ea0dcb7c3dd5228ee8b1312ddad9c70c6af...3f3e2ba7753f8e4434b562d226118a0dfe873498\">diff</a>).</p>\n<h3 id=\"tob-pypi-17-use-of-shelltrue-in-subprocesses\">TOB-PYPI-17:  Use of <code>shell=True</code> in subprocesses</h3>\n<p>An attack vector was identified in the way that cabotage calls out to\n<a href=\"https://github.com/moby/buildkit/tree/master/cmd/buildctl\"><code>buildctl</code></a> when\nrunning container builds in development mode. A specifically crafted user-input\nhas the ability to run arbitrary shell commands on the application host.</p>\n<p>Ultimately this was not determined to be exploitable in the production instance\nof cabotage, since the shell commands were only used when building containers\nin local development mode. The use of <code>shell=True</code> was removed none-the-less as\na matter of hygiene\n(<a href=\"https://github.com/cabotage/cabotage-app/pull/36/files\">diff</a>).</p>\n<h3 id=\"tob-pypi-18-use-of-hmac-with-sha1-for-github-webhook-payload-validation\">TOB-PYPI-18:  Use of HMAC with SHA1 for GitHub webhook payload validation</h3>\n<p>Similar to the SNS verification finding in TOB-PYPI-2 for warehouse,\nthe endpoint that received webhook payloads from GitHub for automated\ndeployments was using SHA1 HMAC signatures to validate authenticity when SHA256\nHMAC signatures were available.</p>\n<p>The remediation of this finding was much more direct than the SNS finding, as\nGitHub began sending the SHA256 signature in the header, does not require\nany changes to the configuration of the webhook, and uses standard HMAC signing\nsupported by the Python standard library\n(<a href=\"https://github.com/cabotage/cabotage-app/pull/37/files\">diff</a>).</p>\n<h3 id=\"tob-pypi-19-potential-container-image-manipulation-through-malicious-procfile\">TOB-PYPI-19:  Potential container image manipulation through malicious Procfile</h3>\n<p>Along the same lines as TOB-PYPI-17, some user-supplied values had the ability\nto alter the cabotage controlled Dockerfile that specifies how release\ncontainers are built, which should not be modifiable.\nThrough specifically crafted process names in the Procfile,\na user could alter the resulting Dockerfile by injecting newlines.</p>\n<p>Remediation was straightforward by adding additional validation of user\nsupplied process names\n(<a href=\"https://github.com/cabotage/cabotage-app/pull/39/files\">diff</a>).</p>\n<h3 id=\"tob-pypi-20-repository-confusion-during-image-building\">TOB-PYPI-20:  Repository confusion during image building</h3>\n<p>Due to a quirk in GitHub's API for fetching references, a given reference may\nreturn a concrete SHA/commit that belongs to a repository other than the one\nspecified in the API call. In this case by providing a reference that resolves\nto a commit on a fork of the configured repository, a user of cabotage had the\nability to intentionally (or mistakenly) configure cabotage to deploy code\nfrom a repository other than the one defined.</p>\n<p>By adding additional validation inspired by <a href=\"https://www.chainguard.dev\">Chainguard</a>'s\n<a href=\"https://github.com/chainguard-dev/clank/blob/a5c8412f4e8fb128d2c0919ec02d89f086afdd24/main.go#L242-L272\">clank</a>\ntool, cabotage now verifies that the resulting SHA for a given reference\nbelongs to the configured repository\n(<a href=\"https://github.com/cabotage/cabotage-app/pull/46/files\">diff</a>).</p>\n<h3 id=\"tob-pypi-21-brittle-x509-certificate-rewriting\">TOB-PYPI-21:  Brittle X.509 certificate rewriting</h3>\n<p>All containers built and deployed by cabotage are done so using short-lived\nauthentication tokens for an internally deployed Docker <a href=\"https://hub.docker.com/_/registry\">registry</a>\ninstance. The cabotage application itself provides this authentication and must\npublish a public key that the registry can use to validate tokens.</p>\n<p>In order to avoid handling private-key material in the application, cabotage\nrelies heavily on <a href=\"https://www.vaultproject.io\">Hashicorp Vault</a>. The transit\nbackend for vault\n<a href=\"https://github.com/hashicorp/vault/issues/3845\">did not support publishing the required x509 certificate</a>\nthat Docker registry required when cabotage was originally developed in 2018,\nso some clever use of the cryptography library was employed to create the\nnecessary file <a href=\"https://github.com/cabotage/cabotage-app/blob/f01b75222280699dcaa99aff3ec60e1d1d1830fc/cabotage/utils/cert_hacks.py\">ref</a>.</p>\n<p>In the audit it was determined that this work around was brittle in the event\nthat an attacker had the ability to alter the length of the signature,\nresulting in an invalid x509 certificate and broken authentication for\nregistry clients.</p>\n<p>In practice, this has not been observed in the five and a half years that\nit has been in production and the result of a successful attack would only lead\nto deployments being halted. As such, we have accepted this finding for the\ntime being and will investigate the newly released x509 support in vault 1.15\nand adopt it if able (<a href=\"https://github.com/cabotage/cabotage-app/issues/43\">issue</a>).</p>\n<h3 id=\"tob-pypi-22-unused-dependencies-in-cabotage\">TOB-PYPI-22:  Unused dependencies in cabotage</h3>\n<p>Similar to TOB-PYPI-3, dependency management for cabotage was lacking. This\nled to a handful of dependencies being installed that could be additional\nexposure to vulnerabilities or attacks.</p>\n<p>By adopting <a href=\"https://pypi.org/project/pip-tools/\"><code>pip-tools</code></a> to compile and pin dependencies, only the projects\nnecessary are installed\n(<a href=\"https://github.com/cabotage/cabotage-app/pull/35/files\">diff</a>).</p>\n<h3 id=\"tob-pypi-24-missing-resource-integrity-check-of-third-party-resources\">TOB-PYPI-24:  Missing resource integrity check of third-party resources</h3>\n<p>When adding support for a new feature, third party JavaScript was added\nwithout subresource integrity information being added.\nThis addition guards against malicious replacement of JavaScript an is good\npractice when loading any third party code.</p>\n<p>Remediation was simple, by ensuring that all CDN loaded JavaScript had the\ncorrect value set\n(<a href=\"https://github.com/cabotage/cabotage-app/pull/40/files\">diff</a>).</p>\n<h3 id=\"tob-pypi-25-brittle-secret-filtering-in-logs\">TOB-PYPI-25:  Brittle secret filtering in logs</h3>\n<p>There was a brief period where cabotage supported building from private GitHub\nrepositories, which necessitated filtering build logs and removing the\nplaintext authentication tokens.</p>\n<p>This filtering was naive, but also no longer required. Remediation was removal\nof the filtering code, and a comment directing a future developer to the\ncorrect way of providing such authentication for builds in the future, should\nbuilding from private GitHub repositories be supported\n(<a href=\"https://github.com/cabotage/cabotage-app/pull/47/files\">diff</a>).</p>\n<h3 id=\"tob-pypi-26-routes-missing-access-controls\">TOB-PYPI-26:  Routes missing access controls</h3>\n<p>Another vestigial piece of code that allowed for the build context necessary\nfor container builds was identified as allowing for potentially non-public\ninformation to be leaked if a release id (UUIDv4) was guessed or surmised.</p>\n<p>This route was unauthenticated as a shortcut rather than adding a new\nauthentication method to cabotage itself.</p>\n<p>This code was made defunct when cabotage began building from contexts pulled\ndirectly from GitHub and supplied via Kubernetes secrets. Remediation was\nagain, a simple removal of the code\n(<a href=\"https://github.com/cabotage/cabotage-app/pull/41/files\">diff</a>).</p>\n<h3 id=\"tob-pypi-28-deployment-hook-susceptible-to-race-condition-due-to-temp-files\">TOB-PYPI-28:  Deployment hook susceptible to race condition due to temp files</h3>\n<p>A final vestigial piece of code was also flagged as part of the audit which\nwas created to fetch and re-package source code from GitHub for deployments.\nThis had the very outside potential of being exploitable if an attacker\ngained access to the filesystem that the cabotage app uses for temporary\nfiles.</p>\n<p>This was similarly made defunct when cabotage began building from contexts pulled\ndirectly from GitHub. Remediation was a final simple removal of code\n(<a href=\"https://github.com/cabotage/cabotage-app/pull/45/files\">diff-0</a>),\nand a refactor of how temporary files are created and opened\n(<a href=\"https://github.com/cabotage/cabotage-app/pull/42/files\">diff-1</a>).</p>\n<h2 id=\"summary\">Summary</h2>\n<p>In addition to the specific findings, the Trail of Bits team also made a number\nof \"Code Quality Recommendations\" and analyzed the overall maturity of the\ncodebase. Those sections of the report highlight one of the two themes I see\nin the report regarding cabotage:</p>\n<ol>\n<li>\n<p>Overall the development experience and continuous integration environment for\ncabotage is lacking.</p>\n</li>\n<li>\n<p>There are countless minutiae that one must consider when writing code with\nsecurity in mind.</p>\n</li>\n</ol>\n<p>In the end, no show stopping or easily exploitable security issues were found,\nwhich is a relief! Many of the most interesting security findings were only\nexploitable by a malicious internal actor who\nalready had configuration permissions in cabotage,\nwas deploying their app there in the first place,\nor had access to the underlying systems.</p>\n<p>The takeaway I have as the sole author and maintainer of cabotage is pretty\nresounding, and addresses both themes from the report:</p>\n<blockquote>\n<p>Projects with solo maintainers do not benefit from the accountability\nthat comes with collaborative development, are prone to deprioritizing\ncritical improvements to developer experience and testing, and don't\nhave the extra sets of eyes that often assist in spotting small bugs or\nimproper handling of security sensitive software.</p>\n</blockquote>\n<p>So if you're interested in infrastructure and projects that make deploying\nsoftware securely and reliably more straightforward, I'd love to talk more.\nSwing by the <a href=\"https://github.com/cabotage/cabotage-app\">cabotage repo</a>\nand consider helping build the software that deploys PyPI,\nand will soon be deploying more and more of the\n<a href=\"https://python.org/psf/\">Python Software Foundation</a>'s infrastructure\nas we migrate from previously gratis PaaS hosting providers.</p>", "image": "https://blog.pypi.org/assets/images/social/posts/2023-11-14-3-security-audit-remediation-cabotage.png", "date_modified": "2026-03-18T18:14:42+00:00", "date_published": "2023-11-14T00:00:02+00:00", "authors": [{"name": "Ee Durbin"}], "tags": ["infrastructure", "security", "transparency"]}]}