A Python package on PyPI called elementary-data, with over 1 million downloads per month, has suffered a supply chain security attack sourced through a GitHub Actions attack vector.
TL;DR
| Advisory | SNYK-PYTHON-ELEMENTARYDATA-16316110 |
| Severity | Critical (CVSS v4.0: 9.3) |
| Affected package | elementary-data==0.23.3 |
| Clean versions | All versions except 0.23.3; upgrade to 0.23.4
|
| Attack type | Supply chain (GitHub Actions CI/CD injection, then credential-stealing package) |
| Stolen credentials | dbt profiles, Snowflake/BigQuery/Redshift creds, AWS/GCP/Azure keys, API tokens, SSH keys, .env files |
| Scope | The PyPI CLI package and a Docker Image got compromised; Elementary Cloud and the Elementary dbt package were not affected |
| Detection marker |
$TMPDIR/.trinny-security-update (Linux/macOS), %TEMP%\.trinny-security-update (Windows) |
| Disclosure | April 25–26, 2026 |
What is elementary-data?
elementary-data is a dbt-native data observability CLI tool used by data and analytics engineers to monitor pipeline health, detect anomalies, and track test failures across data warehouses like Snowflake, BigQuery, Redshift, and Databricks. The package sees roughly 280,000 downloads per week and over 1.1 million per month, placing it firmly in the tier of widely adopted data tooling.
The package provides integrations with most major cloud data platforms, which is precisely what made it an attractive target. A tool that routinely handles connections to Snowflake, BigQuery, and AWS at CI/CD runtime is one that sits next to a lot of valuable credentials.
How the attack unfolded
The compromise occurred in two stages: first, compromising the publication pipeline; second, publishing malicious content that steals further credentials. It’s worth noting that this security incident for the elementary-data package compromise is one of the most prominent vectors recently exploited by TeamPCP and other threat actors.
Stage 1: GitHub actions script injection
On April 24, 2026, at 22:10 UTC, an attacker using a two-day-old GitHub account (realtungtungtungsahur) posted a crafted comment on PR #2147 in the elementary-data repository. The comment exploited a script injection flaw in .github/workflows/update_pylon_issue.yml, a workflow that handled issue/PR comment events.
The vulnerable run: block directly interpolated ${{ github.event.comment.body }} into a shell script before bash parsing occurred. Because this expression is expanded at workflow-template time rather than sanitized as a string argument, injecting shell metacharacters or subcommands through the comment body allows arbitrary code execution in the runner. When the workflow triggered, the attacker's payload ran with the repository's GITHUB_TOKEN in scope.
Critically, the attacker never needed direct write access to the repository. The GITHUB_TOKEN available to the runner had sufficient permissions to create commits, push tags, and dispatch other workflows. The injected handle_comment job stayed active for two hours and forty-six minutes, giving the attacker an extended window to set up each subsequent stage.
Using the stolen token, the attacker forged a release commit with hash b1e4b1f3aad0d489ab0e9208031c67402bbb8480. The commit was structured to look automated and official: it was an orphan commit (unreachable from any branch), authored as github-actions[bot], carried a forged "Verified" PGP signature, and used the message release/v0.23.2 (#2188) — copied verbatim from a legitimate PR merged nine days earlier. The attacker tagged this orphan v0.23.3 and then dispatched the repository's own Release package workflow with tag=v0.23.3 as input. That workflow's checkout step used ref: ${{ inputs.tag || github.ref }}, so it was built directly from the malicious orphan commit without touching master. The legitimate CI/CD pipeline packaged and published the malicious code. By 22:20 UTC, elementary-data==0.23.3 was live on PyPI. A compromised Docker image (ghcr.io/elementary-data/elementary:0.23.3 and :latest, digest sha256:31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255) followed four minutes later.
This attack vector has appeared repeatedly in the PyPI ecosystem. The Ultralytics supply chain attack in December 2024 used the same pull_request_target injection pattern to steal credentials and publish four malicious versions. The LiteLLM compromise in early 2026 took a slightly different path (a poisoned third-party GitHub Action), but the destination was identical: stolen PyPI tokens used to publish a credential-stealing package.
Stage 2: The malicious package
The attacker embedded the malicious payload in a file named elementary.pth, included in the package's site-packages directory.
.pth files are Python path configuration files that site.py, Python's startup module, processes automatically when the interpreter launches. Any line in a .pth file that begins with import is executed as Python code at interpreter startup, before your own code runs. This means the malware activates any time Python starts on the affected system, including during pip install operations, not just when a user explicitly imports elementary.
This technique was also used in the LiteLLM v1.82.8 compromise. It is more persistent and harder to detect than embedding malicious code in __init__.py because it does not require the victim to import the poisoned package. Installing it is sufficient.
Inside the payload: What the malware did
The embedded code in elementary.pth was a credential stealer with three stages of encryption: a base64 outer wrapper, then XOR encryption keyed from an MD5 keystream (seed: swabag), then a second XOR decryption layer. The obfuscation is not sophisticated by modern malware standards, but it is deliberate — it prevents trivial string-based detection of the payload and increases the time required to analyze what the package is actually doing.
Once Python started on an affected machine, the decoded payload:
- Harvested credentials and secrets across the filesystem, targeting a broad set of material:
- dbt profiles (
~/.dbt/profiles.yml) and data warehouse credentials (Snowflake, BigQuery, Redshift, Databricks). - Cloud provider credentials: AWS
~/.aws/credentialsplus live role credentials fetched from the IMDSv2 metadata endpoint, with direct SigV4-signed calls to AWS Secrets Manager and SSM Parameter Store; GCPapplication_default_credentials.json; Azure~/.azure/directories. - SSH private keys (
id_rsa, id_ed25519, ~/.git-credentials). - Container and orchestration secrets:
~/.docker/config.json,~/.kube/config,all/etc/kubernetes/*.conffiles, Kubernetes ServiceAccount tokens. - Package manager credentials:
~/.npmrc,~/.pypirc,~/.cargo/credentials.toml. - Other secrets at rest:
.env*files (scanning up to six directory levels deep),~/.vault-token,~/.netrc,~/.pgpass,~/.my.cnf, API tokens in environment variables. - Cryptocurrency wallet files (Bitcoin, Litecoin, Dogecoin, Zcash, Dash, Monero, Ripple, Ethereum, Cardano, Solana validator keypairs).
- System files:
/etc/passwd,/etc/shadow, shell history files,/var/log/auth.log.
Packed all collected material into an archive named
trin.tar.gz, then exfiltrated it viacurl --data-binaryto the C2 server atigotnofriendsonlineorirl-imgonnakmslmao.skyhanni.cloud, using the HTTP headerX-Rise-To-The-Trinny: agree.Left a marker file at
$TMPDIR/.trinny-security-update(Linux/macOS) or%TEMP%\.trinny-security-update(Windows), indicating the malware executed at least once.
The scope of credentials goes well beyond dbt and data warehouses. The payload is broadly written to sweep whatever secrets are accessible on the machine, including Kubernetes clusters, infrastructure secrets managers, and cryptocurrency keys. The dbt and warehouse targeting make it relevant to the tool's user base, but anyone running this on a developer machine or CI runner stands to lose considerably more.
The credential profile is well-matched to the tool's typical users. Data engineers running the elementary-data CLI are almost certainly using it against a connected data warehouse, with cloud provider credentials, often in a CI/CD environment where those credentials are stored as secrets or environment variables. This is a targeted attack, not a generic spray.
Impact and scope
The attack window ran from April 24, 22:20 UTC (when the package appeared on PyPI) until the package was removed on April 25, between 8:51 and 11:51 UTC, after community members flagged the issue at 6:18 UTC. That is roughly eight to ten hours of exposure.
Anyone to whom the following applies should assume the malware has executed and that their credentials have been exfiltrated:
- ran
pip install elementary-dataor upgraded during that window, - used a Docker image pulled from the elementary-data registry between April 24, 22:24 UTC and removal,
- or had a CI/CD pipeline that automatically pulled the latest version
Elementary Cloud and the Elementary dbt package were not affected, and no other CLI versions contained the malicious code.
Detection: Are You Affected?
Step 1: Check your installed version
pip show elementary-data
If the output shows Version: 0.23.3, your environment was exposed.
Step 2: Check for the execution marker
The malware writes a marker file on execution:
# Linux / macOS (checks $TMPDIR, which defaults to /tmp if unset)
ls -la "${TMPDIR:-/tmp}/.trinny-security-update"
# Windows (PowerShell)
Test-Path "$env:TEMP\.trinny-security-update"
The presence of this file means the credential-stealing code ran in that environment. Its absence does not guarantee safety; the malware may not have written the marker in all execution paths, or the temp directory may have been cleared.
Step 3: Review with Snyk
To check your Python dependencies for this and other known malicious or vulnerable packages:
# Scan your project dependencies
snyk test --file=requirements.txt
# Or for pip-based environments
pip freeze > requirements_check.txt && snyk test --file=requirements_check.txt
Snyk's vulnerability database includes SNYK-PYTHON-ELEMENTARYDATA-16316110 and will flag any environment still pinned to 0.23.3.
Note: We recommend you review our Python security best practices cheat sheet and write-up on Best practices for containerizing Python applications with Docker to practice secure development guidelines.
Remediation
1. Upgrade immediately
pip install --upgrade elementary-data
Version 0.23.4 was published on April 25, 2026, and contains no malicious code.
If you are using a requirements.txt or pyproject.toml, update the pin:
elementary-data>=0.23.4
2. Rotate all credentials that may have been exposed
Treat any credentials accessible to Python processes on affected machines as compromised. Specifically:
-
dbt profiles: Rotate warehouse passwords and OAuth tokens in
~/.dbt/profiles.yml. - Cloud provider keys: Rotate or revoke AWS IAM keys (and check Secrets Manager and SSM Parameter Store for accessed values), GCP service account keys, Azure service principals.
-
Kubernetes: Rotate ServiceAccount tokens, and audit any
/etc/kubernetes/*.conffiles that were accessible. -
Container registries: Rotate credentials stored in
~/.docker/config.json. -
Package manager tokens: Rotate
~/.npmrc,~/.pypirc,~/.cargo/credentials.tomltokens. -
Secrets managers: Rotate HashiCorp Vault tokens (
~/.vault-token) and any.netrc,.pgpass, or.my.cnfcredentials. - SSH keys: If private keys were present on the machine, consider them exposed and rotate them.
- CI/CD secrets: If the affected machine was a CI runner, rotate all secrets stored in that environment.
Rotation alone is not sufficient. Review access logs for the exfiltration domain igotnofriendsonlineorirl-imgonnakmslmao.skyhanni.cloud and for any of your services to detect unauthorized access that may have already occurred.
3. Clear Python caches
# Remove any cached .pth files or bytecode
pip cache purge
# Find and remove any elementary.pth artifacts
find / -name "elementary.pth" 2>/dev/null
4. Pull clean Docker images
The compromised image (ghcr.io/elementary-data/elementary:0.23.3 and :latest) carried digest sha256:31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255. The last known-clean image is 0.23.2 at digest sha256:b3bbfafde1a0db3a4d47e70eb0eb2ca19daef4a19410154a71abee567b35d3d9. Pull a clean image built after April 25, 2026:
docker pull ghcr.io/elementary-data/elementary:latest
Verify you are not running a cached copy of the compromised image:
docker images ghcr.io/elementary-data/elementary --digests
5. Audit your GitHub Actions workflows
If you maintain Python packages, this incident is a prompt to audit any workflow that processes issue or PR comment events. The specific pattern to search for is unquoted context expressions interpolated directly into run: blocks:
# Vulnerable: ${{ github.event.comment.body }} expands before bash parses the command
- run: echo "Comment: ${{ github.event.comment.body }}"
# Safe: pass untrusted input through an environment variable
- run: echo "Comment: $COMMENT_BODY"
env:
COMMENT_BODY: ${{ github.event.comment.body }}
Snyk's post on GitHub Actions vulnerabilities and the TJ Actions compromise analysis covers the broader patterns to look for.
Beyond sanitizing inputs, the more durable fix is to remove long-lived PyPI API tokens from your workflow secrets entirely. PyPI supports Trusted Publishers, which use short-lived OIDC tokens scoped to a specific workflow on a specific repository — tokens that cannot be exfiltrated and reused. The elementary-data attacker needed a long-lived secret to publish; Trusted Publishers eliminate that attack surface. Also, keep privileged release workflows behind manual approval gates that require human confirmation before the publish step runs.
The repeating pattern
This attack follows a now-familiar playbook: find a gap in a project's GitHub Actions configuration, inject code that steals the PyPI publishing token, use that token to publish a malicious version, embed a .pth file or similar startup hook to maximize reach.
The same pattern appeared in the Ultralytics attack (December 2024, pull_request_target branch injection, cryptocurrency miner), the LiteLLM attack (early 2026, poisoned Trivy action, credential stealer with persistent backdoor), and the Cline/Clinejection incident (AI-assisted prompt injection into Actions, stolen tokens).
The pattern is not novel. The tooling to exploit it is well-understood and appears to be in active, repeated use. For package maintainers, the priority is workflow hardening: restrict the use of pull_request_target, require manual approval for release workflows, use short-lived OIDC tokens for PyPI publishing instead of long-lived API tokens, and implement branch protection rules to prevent unauthorized release triggers.
For elementary-data users, the Elementary team responded quickly: from community report to initial remediation was under four hours, and the team published a full incident report. That response tempo is worth noting alongside the incident itself.