In May 2022, a small, unassuming Python utility called ctx — a package that had quietly done one job (turning dictionaries into dot-accessible objects) since 2014 — became one of the most-cited examples of how fragile open source package ownership can be. The ctx package hijack didn't involve a zero-day, a leaked credential dump, or a nation-state actor. It involved something far more mundane: a maintainer's personal domain expired, an attacker bought it, and used it to walk straight into the maintainer's PyPI account.
Security researcher Yee Ching Tok, writing for the SANS Internet Storm Center, first flagged the compromise after noticing that ctx had shipped several new versions overnight — versions that reached out to an external server and exfiltrated environment variables, including AWS credentials, on every import. Almost simultaneously, a PHP library called phpass was hit with the identical technique. This is the story of how it happened, what it stole, and what it says about the trust model underneath every pip install.
What Was the ctx Package Hijack, Exactly?
The ctx package hijack was the takeover of a legitimate, long-dormant PyPI package after an attacker gained control of its maintainer's associated email domain. ctx had last been meaningfully updated around 2014, sat at version 0.1.2, and was pulled in as a transitive dependency by other tools rather than installed directly by most developers. That obscurity is exactly why it was a good target: it had real, if modest, download volume, an inattentive maintainer, and effectively zero ongoing scrutiny. In May 2022, new releases — 0.1.2 followed by 0.2.2, 0.2.6, 1.99.0, 2.0.0, and 2.1.0 — appeared on PyPI within a short window, each one modified to harvest and transmit the importing machine's environment variables to a remote endpoint hosted on Heroku. Because ctx was still being resolved by build systems and CI pipelines that had it pinned or referenced indirectly, the malicious versions propagated into environments that had nothing to do with the original developer's intent.
How Did an Expired Domain Lead to a Maintainer Account Takeover?
It happened because PyPI, like most package registries, treats a successful "forgot password" email as proof of maintainer identity — and that email loop is only as trustworthy as the domain it points to. The ctx maintainer's PyPI account was registered years earlier using an email address at a personal domain. At some point that domain lapsed and was not renewed. The attacker noticed the expired domain, registered it for themselves, stood up mail service on it, and then simply used PyPI's standard password-reset flow: request a reset link, receive it at the now attacker-controlled mailbox, and log in as the legitimate maintainer. No phishing, no credential stuffing, no exploited vulnerability in PyPI's infrastructure — just a maintainer account takeover made possible by an unmonitored piece of internet real estate quietly changing hands. It is a textbook illustration of how identity verification built on email is only as durable as the weakest, most easily forgotten link in that chain: a domain renewal that nobody was watching.
What Did the Malicious ctx Versions Actually Do?
Once inside the account, the attacker's payload was simple and effective: read os.environ on import and POST the contents to a hardcoded external URL. That meant any secret exposed as an environment variable — AWS access keys, database credentials, API tokens, CI/CD secrets — could be silently shipped off to the attacker's server the moment a vulnerable version of the Python package was installed and imported, with no further action required from the victim. There was no ransomware, no destructive payload, no attempt to move laterally inside the compromised system. It was a quiet, high-yield credential-harvesting operation designed to stay under the radar for as long as possible, riding on the reputation of a package that had existed innocuously for nearly a decade.
Why Did a Nearly Identical Attack Hit a PHP Package at the Same Time?
Because the same researcher who bought the expired ctx domain applied the identical playbook to phpass, a PHP password-hashing library, within roughly the same window in May 2022. The phpass compromise used the same expired-domain-to-email-reset trick to gain control of the maintainer's GitHub account, then modified the code to exfiltrate data using the same Heroku-hosted endpoint that received the stolen ctx environment variables. The overlap in infrastructure is what allowed researchers to connect the two incidents quickly and treat them as a single campaign rather than two coincidental compromises. It also made a broader point unmistakable: this wasn't a PyPI-specific flaw or a Python-specific flaw. It was a registry-agnostic technique — anywhere an account recovery flow trusts an email address, an expired domain is a viable attack surface, whether the ecosystem is PyPI, npm, RubyGems, or Packagist.
What Does the ctx Incident Reveal About Package Registry Security?
It reveals that package registries were largely designed around the assumption that a maintainer's identity is stable, when in practice maintainer identities decay constantly — domains lapse, email providers get abandoned, phone numbers get recycled, and two-factor methods go stale. PyPI has since expanded requirements around two-factor authentication for maintainers of critical projects and added tooling to detect anomalous release patterns, but at the time of the ctx incident, an account with no 2FA and a single point of email-based recovery was still the norm rather than the exception. The incident also exposed how much of the ecosystem's security depends on the vigilance of individual maintainers who are, in most cases, unpaid volunteers with no obligation or budget to monitor their own domain renewals indefinitely. A Python package doesn't need a sophisticated vulnerability to become a supply chain weapon; it just needs one forgotten renewal notice.
Could This Happen Again, and How Would Anyone Notice?
Yes, and the honest answer is that most organizations would notice only after the damage was done, because very few teams actively diff the behavior of a dependency between versions. The ctx hijack was caught because a researcher happened to notice unusual release activity and inspected the code by hand — not because any automated system in the affected organizations' pipelines flagged that a trusted, years-stable dependency had suddenly started making outbound network calls to harvest environment variables. That detection gap is the real lesson: version pinning and lockfiles reduce exposure to opportunistic typosquats, but they do nothing to stop a legitimately hijacked package from pushing a malicious update that gets pulled in on the next routine pip install or dependency bump. Any package, no matter how old or boring, is a potential vector the moment its maintainer's account can be taken over.
How Safeguard Helps
Safeguard is built for exactly this failure mode — the moment when a package you already trust starts behaving like something you'd never have approved. Rather than relying on static allowlists or reputation scores that only catch known-bad packages after the fact, Safeguard continuously monitors the behavior of dependencies pulled into your build and runtime environments, flagging changes like unexpected outbound network calls, sudden access to environment variables or credential stores, and anomalous code changes introduced in a new release of a previously dormant Python package.
Safeguard also tracks maintainer and publishing signals across PyPI, npm, and other registries — surfacing red flags such as a long-idle package suddenly shipping multiple releases in quick succession, a maintainer account publishing from a new, unfamiliar pattern, or ownership changes that don't match the project's public history. Combined with software bill of materials (SBOM) visibility across your dependency tree, this means a repeat of the ctx package hijack — an expired domain, a quiet maintainer account takeover, and a malicious update slipped past routine dependency updates — gets caught at the behavioral layer, before stolen environment variables ever leave your infrastructure. In a supply chain where trust is inherited transitively through thousands of packages you'll never personally audit, that continuous, behavior-based verification is what closes the gap that email-based account recovery was never built to defend.