A cached Python file exposed a PyPI administrator's GitHub token
A local credential survived in compiled Python bytecode after the source changed. The incident shows why a clean source review cannot establish what a container build publishes.
Restoring a source file does not restore every artifact produced while that file contained a secret. A Python bytecode cache can preserve the earlier value even after the visible source no longer contains it. That distinction explains the GitHub credential leak described in PyPI's incident report and the researchers' July 9 account. A compiled file containing a personal access token entered a public container image.
The exposed credential belonged to Ee Durbin's GitHub account. PyPI reports notification on June 28, immediate revocation, and a review that found no indicators of malicious activity. This was a GitHub personal access token, not a package upload token. The distinction identifies the authority exposed and prevents the incident from being described as a disclosure of every package maintainer's registry credentials.
I find this relevant to Python's packaging machinery because generated files are easy to treat as disposable implementation details. Their role as caches does not make their contents harmless. A distribution process that copies them has to consider what they contain, including data from a development state that no longer appears in the source selected for publication.
The artifact retained a local development change
The incident report describes a local modification made while developing Cabotage's build functionality. Anonymous GitHub API rate limits prompted use of a personal token in local code. Production used a GitHub App. The staging build process attempted to put local source changes aside before building, but bytecode generated through a shared Docker volume remained. The build's ignore configuration did not exclude those caches.
That sequence creates two separate histories in the same workspace. Source restoration changes the text the developer sees. Cached output can continue to describe an earlier execution of that text. A container build that includes both can publish material from both histories. Reviewing the restored source alone answers a narrower question than inspecting everything supplied to the build.
JFrog's researchers describe finding the token in a compiled file while the corresponding source lacked it. Their account emphasizes scanning distributed binaries as well as source. The important observation is the discrepancy between the two representations. A search that inspected only the current text would not have established the contents of the image being distributed.
Python documents bytecode as a separate file
The Python 3.12.4 py_compile documentation describes compiling source and writing the resulting bytecode to a separate cache file. By default, that path uses the __pycache__ convention and a .pyc suffix. The documentation also distinguishes the source filename from the destination used for the compiled output. Those separate paths are enough to explain why changing one file is not equivalent to deleting the other.
The security implication does not require the cached file to execute during a future deployment. Disclosure can occur as soon as someone can inspect the artifact that contains it. A cache that the runtime later ignores can still expose a string. Runtime invalidation rules and publication confidentiality therefore answer different questions. The former concerns whether cached code remains suitable for execution, while the latter concerns who can recover its contents.
This distinction also changes what evidence a clean build provides. Starting with selected source in a new workspace excludes unrelated local outputs by construction. Reusing a development directory requires the build process to identify every output that must not participate. That second approach places more responsibility on exclusion rules and on assumptions about the workspace's contents. A build's input directory is consequently part of the release definition, including files that the source-control system does not track.
Cabotage belongs to the deployment system
PyPI's 2023 Cabotage audit remediation report explains the project's role. Cabotage deploys Warehouse and supporting services into Kubernetes. It aims to provide automated deployment and self-service configuration while reducing dependence on a particular hosting provider. The report treats the deployment codebase separately from Warehouse, which implements PyPI itself.
That context matters when describing potential consequences. A credential in deployment tooling can expose authority beyond the application feature being developed. The relevant permissions come from the credential's account and scope. They do not become narrower merely because the developer inserted it into a helper used to fetch a build file. Local convenience can consequently import broader account authority into a small implementation detail.
The audit report and the present incident also concern different evidence. A review of a service's code can find implementation weaknesses. It cannot establish that every later container image contains only the reviewed source and intended outputs. Build inputs and credential handling remain part of the operational system after an audit completes. Treating those as continuing responsibilities does not diminish the value of reviewing the code.
Revocation and artifact removal have different effects
The incident response describes moving Cabotage's deployment builds to clean source checkouts and an internal registry. That changes how local development output can reach published builds. Revoking the token addressed a different part of the incident, ending the authority of the exposed credential. Neither action should be used as evidence for the other. An image can remain obtainable after a token becomes unusable, and removing an image cannot establish that nobody copied a still-valid token.
I regard the clean-checkout change as the most useful engineering response in the report. It removes dependence on the exact collection of caches left by local development. Credential expiry limits how long a leaked value can authorize requests, while artifact selection limits which values can escape in the first place. Both controls have a specific role in this failure sequence.
The reported investigation found no malicious activity, which is narrower than proving the credential was never observed. That is the appropriate distinction to retain when explaining the outcome. The concrete failure was an exposed credential in distributed bytecode. The response revoked that authority and changed the source of future deployment builds so local cached files would no longer enter them through the same workflow.