Safeguard
Supply Chain Attacks

The elementary-data hijack: when a dbt observability tool became a credential harvester

A hijacked GitHub Actions token let attackers publish a backdoored elementary-data release that stole cloud, warehouse, and SSH credentials.

Safeguard Research Team
Research
6 min read

On April 24, 2026 at 22:20:47 UTC, a malicious version of elementary-data — a dbt-native data observability CLI with roughly 280,000 downloads a week on PyPI — landed on the package index under the version number 0.23.3. It wasn't uploaded by the maintainers. Attackers had exploited a GitHub Actions script-injection flaw in the project's CI workflow, tricking it into leaking its GITHUB_TOKEN through a crafted pull-request comment, then used that token to forge a release. The payload rode inside a .pth file, a mechanism Python's own site.py treats as trusted: any line in a .pth file starting with import executes automatically at interpreter startup, on every single python invocation, whether or not anyone ever imports the package. The backdoor went after dbt profile credentials, AWS/GCP/Azure tokens, SSH keys, Git credentials, Kubernetes service account tokens, Docker configs, .env files, and even cryptocurrency wallets. The community flagged it within hours — 6:18 UTC on April 25 — and PyPI and GHCR pulled the release between 8:51 and 11:51 UTC that morning, an exposure window of roughly eight to ten hours before a clean 0.23.4 shipped. This post walks through what happened, why data-pipeline tooling specifically was such a lucrative target, and what defenders should take from it.

What actually happened in the elementary-data compromise?

The attack chain started nowhere near PyPI — it started in GitHub Actions. elementary-data's repository ran a workflow that interpreted untrusted text from a pull-request comment as shell input, a pattern security researchers call script injection: attacker-controlled strings get concatenated into a run: step and executed as code rather than treated as data. That let the attacker exfiltrate the workflow's GITHUB_TOKEN, a credential scoped to push commits, create tags, and — critically for a package that publishes via CI — trigger a release. With that token in hand, the attacker forged a release tag, which kicked off the project's publish pipeline and pushed a backdoored 0.23.3 build straight to PyPI, alongside a matching malicious image on GitHub Container Registry. No maintainer credentials were phished, no PyPI account was taken over — the entire compromise ran through a CI token that should never have been reachable from a PR comment in the first place.

Why does a .pth file make such an effective backdoor?

Python's site module scans every .pth file in site-packages at interpreter startup and executes any line beginning with import as live code, before your program's first line ever runs. It's a long-standing feature meant for path manipulation by tools like setuptools and virtual environments, not a sandboxed hook — whatever runs there has full process privileges. The elementary-data payload shipped an elementary.pth file that exploited exactly this: installing the package was enough to plant the trigger, and simply running any python command afterward — not import elementary, not even touching the CLI — fired the credential harvester. That's a meaningfully wider blast radius than a malicious __init__.py, because it doesn't require the victim to use the package at all, only to have it installed in an active environment, which describes most CI runners and data-engineer laptops that touch dbt.

Why are data-pipeline tools an underestimated credential-theft target?

Observability and orchestration tools like elementary-data, dbt itself, Airflow providers, and Fivetran/Airbyte connectors are, by design, granted the broadest credential footprint in a modern data stack. They need live connections to Snowflake, BigQuery, Redshift, or Databricks warehouses, plus the cloud IAM tokens that authenticate those connections, often from the same CI runners and orchestration hosts that also hold Git and container-registry credentials. Security teams routinely harden application-facing services — API gateways, web frameworks, auth libraries — because those are the obvious attack surface. A CLI that a data engineer pip installs locally to check pipeline freshness rarely gets the same scrutiny, yet it runs with warehouse-admin-adjacent permissions and typically executes on every CI job and every analyst's machine. That combination — high download volume (over a million a month for this package), broad ambient credentials, and low perceived risk — is exactly the profile that makes a dependency worth backdooring rather than a bespoke phishing campaign.

How is this different from a typical typosquat or dependency-confusion attack?

Typosquatting and dependency-confusion attacks rely on tricking a build into installing the wrong package — a similarly named malicious upload, or an internal package name shadowed by a public one, as researcher Alex Birsan demonstrated across more than 30 companies in 2020–2021. The elementary-data incident is the more dangerous variant: the real, correctly-named package itself was compromised at the source. Every downstream check that trusts "this is the package I asked for, from the maintainer I trust" passed, because from PyPI's perspective it was a normal, authorized release published through the project's own CI pipeline. That's why GitHub Actions script injection has become a recurring root cause across supply-chain incidents industry-wide — compromising a CI token doesn't just leak secrets once, it hands the attacker a legitimate publishing channel that inherits every downstream user's trust in the project.

What should teams do differently after an incident like this?

First, treat CI workflow permissions as a credential-management problem, not a convenience default: scope GITHUB_TOKEN to the minimum required permissions per job, avoid interpolating untrusted PR titles, branch names, or comment bodies directly into run: steps, and use pull_request_target only with extreme care given it runs with access to repository secrets against attacker-controlled code. Second, pin dependencies to hashes ( pip install --require-hashes ) rather than floating version ranges, so a rogue release published minutes after your last successful build doesn't get silently pulled into the next pip install in CI. Third, isolate the credentials that data-pipeline tools use — short-lived, narrowly scoped warehouse and cloud tokens rather than long-lived admin keys sitting in a shared .env — so that a compromised dependency's blast radius is a single rotate-and-revoke rather than a full incident response.

How Safeguard helps

Detecting an incident like this after the fact means diffing a "clean" release against a compromised one across every artifact your build produced — the wheel, the sdist, and any container image published alongside it. Safeguard's SBOM generation captures exactly that inventory automatically on every build, so when an advisory drops for a dependency like elementary-data, teams can query which services actually resolved the affected version instead of re-scanning from scratch. Paired with dependency scanning that flags newly published or unusually-timed releases of packages already pinned in a manifest, and reachability analysis that shows whether a flagged package version is actually installed in a running environment versus sitting unused in a lockfile, Safeguard turns "was I affected by the elementary-data compromise" from a multi-day forensic exercise into a query you can run the same morning the advisory ships.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.