Back to Blog

elementary-data Compromised on PyPI and GHCR: Forged Release Pushed via GitHub Actions Script Injection

A malicious version of elementary-data (0.23.3) was published to PyPI and is, at the time of writing, still listed as the latest release. The same release run also pushed a multi-arch container image to GitHub Container Registry at ghcr.io/elementary-data/elementary, tagged both 0.23.3 and latest.
Varun Sharma
View LinkedIn

April 25, 2026

Share on X
Share on X
Share on LinkedIn
Share on Facebook
Follow our RSS feed
Table of Contents

A malicious version of elementary-data (0.23.3) was published to PyPI and is, at the time of writing, still listed as the latest release. elementary-data is a widely deployed Python package for dbt data observability.

The same release run also pushed a multi-arch container image to GitHub Container Registry at ghcr.io/elementary-data/elementary, tagged both 0.23.3 and latest. Every unpinned docker pull ghcr.io/elementary-data/elementary and every FROM ghcr.io/elementary-data/elementary line without a pinned tag has been pulling the trojaned image since April 24.

The attacker exploited a script injection vulnerability in one of the project's own GitHub Actions workflows, then used the workflow's GITHUB_TOKEN to forge a signed release commit and dispatch the legitimate publishing pipeline against it — without ever touching the master branch or opening a pull request.

UPDATE: The Elementary team removed elementary-data 0.23.3 from PyPI and the matching malicious image from GHCR. They have since published a clean replacement, 0.23.4, and the :latest tag now resolves to the clean image. The maintainer's full incident notice is in issue #2205.

The compromise was reported by crisperik in issue #2205 on April 25, 2026, and shortly afterwards confirmed by H-Max, who also escalated it directly on the Elementary community Slack.

Issue #2205 — the original community report.

The Compromised Release

elementary-data==0.23.3 was uploaded to PyPI on April 24, 2026 at 22:20:47 UTC. Both the wheel and the source distribution contain a single malicious addition compared to 0.23.2: a top-level elementary.pth file. Python automatically discovers .pth files in site-packages and execs any line beginning with import at interpreter startup — meaning the payload fires on every Python invocation in any environment where the package is installed, not only on import elementary.

PyPI listing for elementary-data 0.23.3. The malicious release sits at the top of the version history, marked as the latest release. It was uploaded on April 24, 2026 at 22:20:47 UTC and remains live and unyanked at the time of writing

The corresponding GitHub release is the giveaway. The release name, dsajdkjsajkdsajk, and body, dsakdjsakjdjsa, are gibberish - an attacker keyboard-mash that no maintainer would have signed off on. The release was created by github-actions[bot], not by a human, and is still labelled "Latest" on the project page:

The v0.23.3 GitHub release. Compare the title and body — dsajdkjsajkdsajk and dsakdjsakjdjsa — against the prior release, Python v0.23.2, which follows the project's normal naming convention.

The v0.23.3 Git tag points at commit b1e4b1f3. That commit changes exactly two things: it bumps pyproject.toml to 0.23.3, and it adds elementary.pth — a single line, ~245 KB of base64:

The malicious elementary.pth file shipped inside the wheel

The Docker Image Is Compromised Too Including :latest

The same Release package workflow that uploads to PyPI also has a build-and-push-docker-image job. Both jobs ran successfully against the orphan-tagged commit and finished in the same workflow run. The result is a multi-arch (linux/amd64 + linux/arm64) image at ghcr.io/elementary-data/elementary, tagged with both 0.23.3 and latest, that carries the same payload as the wheel:

  • Compromised: ghcr.io/elementary-data/elementary:0.23.3 → digest sha256:31ecc5939de6...634255
  • Compromised (same digest): ghcr.io/elementary-data/elementary:latest → digest sha256:31ecc5939de6...634255
  • Clean: ghcr.io/elementary-data/elementary:0.23.2 → digest sha256:b3bbfafde1a0...35d3d9

The image landing at :latest is the consequential part. Many teams pin Python package versions in requirements.txt or lockfiles but use latest (or no tag at all, which defaults to latest) for container images. Anyone running a Kubernetes deployment, an Argo CD application, a Docker Compose stack, or a Dockerfile FROM line that does not pin the image by digest has been pulling the trojaned build since April 24.

The Payload: A Three-Stage Credential Stealer

The .pth file decodes a base64 wrapper, which then walks two more layers of XOR-with-MD5-keystream encryption before reaching the actual collector. Two cipher seeds are baked into the payload as cleartext strings:

  • swabag — seed for stage 1 → stage 2
  • for any questions: contact 050afbe046d7545f5af1a0d3fcfbaf6e993fd93d487b431f09bc9e963c7220a135 on session — seed for stage 2 → stage 3 (the string also functions as the actor's contact channel)

Stage 3 is a comprehensive credential and secret harvester. It reads from disk and from live cloud APIs:

  • Identity: all SSH private keys, authorized_keys, known_hosts, ~/.git-credentials, gh auth token
  • Cloud (file + live API): AWS credentials and IMDSv2 role lookup, then SigV4-signed direct calls to AWS Secrets Manager (ListSecrets/GetSecretValue) and SSM Parameter Store; GCP application_default_credentials.json; Azure ~/.azure
  • Container & orchestration: ~/.docker/config.json, ~/.kube/config, all /etc/kubernetes/*.conf, ServiceAccount tokens, kubectl get secrets --all-namespaces
  • Secrets at rest: every .env* reachable to depth 6, ~/.npmrc, ~/.pypirc, ~/.cargo/credentials.toml, ~/.vault-token, ~/.netrc, ~/.pgpass, ~/.my.cnf
  • Crypto wallets: Bitcoin, Litecoin, Dogecoin, Zcash, Dash, Monero, Ripple wallet configs and wallet*.dat; Ethereum keystores; Cardano keys; Solana validator and identity keypairs (validator-keypair.json, id.json, etc.); Anchor target/deploy keys
  • System: /etc/passwd, /etc/shadow, shell histories, /var/log/auth.log

The collected output is tar-gzipped into trin.tar.gz and POSTed in a single request via curl --data-binary to:

https://igotnofriendsonlineorirl-imgonnakmslmao.skyhanni.cloud/
Header: X-Rise-To-The-Trinny: agree

How the Forged Release Reached PyPI and GHCR

Step 1: A comment that became code

The first move was a single comment on an open pull request. PR #2147 is a long-lived master → docs sync PR opened automatically by the project's release pipeline back in March, sitting open in the queue. The attacker dropped a single line into the comment box:

The comment that started the attack. The author realtungtungtungsahur is a 2-day-old GitHub account with no prior activity.

The vulnerable workflow is .github/workflows/update_pylon_issue.yml. Its first step interpolates the comment body directly into a run: block:

The unsafe line in update_pylon_issue.yml. The expression ${{ github.event.comment.body }} is expanded by the runner into the shell script before bash parses it.

This is a textbook GitHub Actions script injection vulnerability. The same class of bug appears across github.event.issue.title, github.event.pull_request.title, github.head_ref, and similar fields. When the comment fired the workflow at 22:10:14, the runner started a handle_comment job with the repository's GITHUB_TOKEN in scope, then immediately executed the attacker's curl | bash stager:

The injected payload running inside the workflow. The job log shows the curl | bash command from the comment body executing as part of the Extract Issue or Pull Request Details step.

Step 2: Forging a release commit, then keeping the runner alive

The forged commit's message, release/v0.23.2 (#2188), is a verbatim copy of an unrelated, legitimate PR title from nine days earlier:

The forged commit b1e4b1f3. Author github-actions[bot], committer web-flow, green "Verified" PGP badge, message release/v0.23.2 (#2188). The diff adds elementary.pth at the repository root and bumps pyproject.toml to 0.23.3.

The original handle_comment job did not finish quickly. It kept running for over two and a half hours before finally exiting with status failure — long enough for the rest of the attack to play out under the cover of an "in-progress" workflow run:

The hung comment-trigger run. Triggered by realtungtungtungsahur at 22:10:14 UTC, conclusion failure, total runtime 2 hours 46 minutes.

Step 3: Dispatching the legitimate publishing pipeline

With the malicious commit in place and the v0.23.3 tag pointing at it, the attacker called the GitHub API to dispatch the Release package workflow with input tag=v0.23.3. The workflow's checkout step uses ref: ${{ inputs.tag || github.ref }}, so it built straight from the orphan-tagged commit:

The Release package run that did the publishing. Event: workflow_dispatch. Triggering actor: github-actions[bot]
The PyPI publish step running against the orphan commit. Inside publish-to-pypi, the pypa/gh-action-pypi-publish action uploads the freshly built wheel and sdist to PyPI using the project's stored PYPI_USER / PYPI_PASS secrets.

How StepSecurity Detects and Prevents This

Threat Intelligence: 24x7 SOC and Threat Center

StepSecurity operates a 24x7 Security Operations Center that continuously monitors npm, PyPI, GitHub Actions, and the wider open-source ecosystem for supply chain attacks. When the SOC confirms a compromise, the StepSecurity Threat Center publishes a real-time advisory to our customers with "Am I Affected?" links pre-wired to that tenant's own codebase, CI baselines, and developer-machine inventory and the relevant indicators are pushed to downstream protections (Harden-Runner global block list, Compromised Package Check, Developer MDM, etc.) so they take effect across every workflow without any customer configuration change.

The Threat Center advisory for this incident. Customers see the alert with "Am I Affected?" links resolved to their own organization, the C2 domain pinned for baseline lookup, and the compromised package and image versions ready to feed into the codebase, CI, and developer-machine searches.

Block C2 traffic with Harden-Runner

Harden-Runner monitors every outbound connection from your GitHub Actions runners. The C2 domain has been added to the Harden-Runner global block list, so every workflow using Harden-Runner now refuses the curl POST to igotnofriendsonlineorirl-imgonnakmslmao.skyhanni.cloud before any credential leaves the runner.

Harden-Runner blocking the C2 callback. Network event view from a controlled run that installed elementary-data==0.23.3 and tried to reach the C2. Harden-Runner identifies the outbound call, classifies it as an attack, and refuses the connection. You can inspect the live run for yourself: Harden-Runner Insights — Network Events.

Block known-compromised packages at PR time

The PyPI Compromised Package Check blocks pull requests that introduce a known-malicious version. elementary-data==0.23.3 has been added to the list. The PyPI Package Cooldown Check would also have flagged a PR that adopted 0.23.3 for being too newly published to trust, holding it for review until the cooldown window elapsed.

The PR-time check in action. A pull request that introduces elementary-data==0.23.3 fails the StepSecurity Compromised Package Check, with the run output explaining why the version is blocked and pointing reviewers at the corresponding advisory.

Discover affected developer machines

StepSecurity Developer MDM inventories Python packages installed across enrolled developer machines so you can identify exposure within minutes of a disclosure.

Indicators of Compromise

  • Compromised PyPI package: elementary-data==0.23.3
  • Last clean PyPI version: 0.23.2
  • Compromised container image: ghcr.io/elementary-data/elementary:0.23.3 and :latest (both at digest sha256:31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255; multi-arch linux/amd64 + linux/arm64)
  • Last clean container image: ghcr.io/elementary-data/elementary:0.23.2 (digest sha256:b3bbfafde1a0db3a4d47e70eb0eb2ca19daef4a19410154a71abee567b35d3d9)
  • Injection file: elementary.pth at the package root (single base64-wrapped line, ~245 KB)
  • Git tag: v0.23.3 → commit b1e4b1f3aad0d489ab0e9208031c67402bbb8480 (orphan; not reachable from any branch)
  • C2 / exfiltration domain: igotnofriendsonlineorirl-imgonnakmslmao.skyhanni.cloud
  • Exfiltration header: X-Rise-To-The-Trinny: agree
  • Exfiltration archive: trin.tar.gz (created in temp dir; auto-cleaned post-upload)
  • Persistent execution marker: $TMPDIR/.trinny-security-update
  • Stager URL (expired): https://litter.catbox.moe/iqesmbhukgd2c7hq.sh
  • Attacker GitHub account: realtungtungtungsahur (created April 22, 2026)
  • Actor contact (Session ID): 050afbe046d7545f5af1a0d3fcfbaf6e993fd93d487b431f09bc9e963c7220a135

Acknowledgements

Credit for this disclosure belongs to two members of the elementary-data community whose quick action shortened the exposure window:

  • crisperik — identified that the v0.23.3 release contained malicious base64-encoded code, recognised the similarity to the recent litellm compromises, and opened issue #2205 at 06:18 UTC on April 25.
  • H-Max — independently confirmed the report on the issue minutes later and posted to the Elementary community Slack so a maintainer would see it directly, accelerating the response.

We also recognise the Elementary team for their fast investigation and remediation: the malicious artifacts were removed from PyPI and GHCR within hours of the report, a clean replacement (0.23.4) was published the same day, and a transparent public incident notice was posted on issue #2205.

Blog

Explore Related Posts