
How GHAPPIER Turned npm Trusted Publishing Into a Package Poisoning Pipeline
Sixty-five GitHub repositories compromised and one npm package poisoned to deliver a loader named GHAPPIER: that was the story on 2026-09-22, and most coverage led with the repo count. This post looks at the part that actually matters — how the malicious release rode npm's trusted publishing path, and how an attacker can turn CI/CD trust into a package poisoning pipeline.
The count is the boring part. What matters is the delivery mechanism: the publish went out through npm trusted publishing, the OIDC flow that exists so maintainers never have to keep a long-lived NPM_TOKEN sitting in GitHub secrets.
My position: trusted publishing closed off one class of credential theft and opened a narrower, sharper trust boundary in its place — whatever code runs inside the publishing job. An attacker who can steer that job doesn't need your token. The workflow mints one for them, on demand, correctly signed, provenance attached.
What the GHAPPIER reports actually say happened
Three items landed in the same news window. I'm keeping them separate, because the sources do, and nothing in the seed material connects them.
| Report | Published | Claim |
|---|---|---|
| CyberSecurityNews | 2026-09-22 | GHAPPIER supply chain attack compromised 65 GitHub repositories and poisoned an npm package, abusing npm trusted publishing |
| MSSP Alert | 2026-09-22 | A new malware loader named GHAPPIER abused npm trusted publishing |
| Dark Reading | 2026-09-22 | The Shai-Hulud attack reached CrowdSec's GitHub data |
CyberSecurityNews carries both the repo count and the npm-publish claim. The MSSP Alert headline is where the loader name comes from. Dark Reading describes a different campaign against a different target, and I won't fold it into GHAPPIER without evidence that public reporting doesn't currently contain.
Confirmed facts versus what the public details do not establish
Established by the reports: 65 GitHub repositories compromised, one npm package poisoned to distribute a loader named GHAPPIER, and abuse of npm trusted publishing as the publish mechanism.
Not shown by the public details: the initial access vector, the loader's full capabilities, which package and which versions were affected, how those 65 repositories were selected, whether Shai-Hulud and GHAPPIER share tooling or operators. There is no CVE in the source set, no advisory ID, no version range. Anything more specific than the above would be me inventing detail, so I won't. What follows is architecture analysis: how the trusted-publishing path works, where it can be steered, and what a maintainer can actually check today.
How npm trusted publishing with GitHub OIDC is supposed to work
On npmjs.com, a package owner registers a trusted publisher: the GitHub organization, repository, workflow filename, optionally a GitHub Environment name. From then on, the registry accepts a publish that arrives with an OIDC token whose claims match that configuration. No static token is involved.
GitHub needs one permission on the job that publishes: id-token: write. The npm CLI requests an OIDC token from GitHub's token service, exchanges it with the registry, and receives a short-lived credential scoped to that single run. With --provenance, the CLI also builds a SLSA provenance statement describing the build and has it signed through Sigstore, then attaches it to the published version.
name: publish
on:
push:
tags: ["v*"]
permissions:
contents: read
jobs:
publish:
runs-on: ubuntu-latest
environment: npm-publish
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@<40-char-commit-sha>
- uses: actions/setup-node@<40-char-commit-sha>
with:
node-version: 22
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm publish --provenance --access public
Notice what is not in that file: NPM_TOKEN, secrets.NPM_TOKEN, or any repository secret at all. That absence is the whole point of the design.
The registry's view of a published version exposes the relevant fields through the packument:
$ npm view [email protected] --json | jq '{version, integrity: .dist.integrity, attestations: .dist.attestations}'
The output shape looks like this (illustrative — these are the field names to look for, not a capture from a specific package):
{
"version": "1.4.2",
"integrity": "sha512-<base64 sha512 of the tarball>",
"attestations": {
"url": "https://registry.npmjs.org/-/npm/v1/attestations/[email protected]",
"provenance": { "predicateType": "https://slsa.dev/provenance/v1" }
}
}
No attestations key means no provenance, regardless of how the README reads.
Where the npm trusted-publishing trust chain actually breaks
The chain runs: tag push → workflow triggered → runner starts → job executes steps → OIDC token requested → registry validates claims → publish. Every arrow after "runner starts" is influenced by workflow content.
That hands an attacker a list of ordinary, boring CI patterns to work with:
| Pattern | What it hands over |
|---|---|
pull_request_target plus checkout of github.event.pull_request.head.sha | Attacker-authored code executing in a job with base-repo permissions |
Third-party action pinned to @v3 instead of a commit SHA | Whoever controls that tag controls your job's code |
npm ci on a lockfile the pull request can edit | Arbitrary postinstall execution inside the workflow |
actions/cache restore keys shared with PR jobs | Poisoned files consumed later by the publish job |
| Artifact upload in a PR job, download as the publish input | Attacker controls the tarball while provenance points at your repo |
Secrets or id-token: write granted at workflow level | Every step in every job inherits the capability |
To be explicit: this is design analysis of the OIDC publish path, not a claim about what GHAPPIER did. The reports never describe the intrusion chain.
The two things the attacker needs
Stripped to objectives, an attacker needs (1) code execution inside the publishing job's context and (2) reach at a moment when the OIDC token can be requested — either the publish step itself or any step that can write to the workspace the publish step will use.
Step one is often cheap, as the table shows. Step two tends to be free, since the token is requested by the same job that runs the build. Framing this as a build-graph problem rather than a malware problem is the useful reframe.
Detecting a poisoned npm publish and a hijacked workflow
These are checks a maintainer can run without any vendor tooling.
## 1. What the registry claims it published
npm view <pkg>@<version> --json | jq '.dist'
## 2. Fetch the exact tarball the registry serves and hash it
npm pack <pkg>@<version>
## dist.integrity is base64-encoded sha512; hash the tarball the same way
openssl dgst -sha512 -binary <pkg>-<version>.tgz | openssl base64 -A
If the two base64 strings differ, the bytes on the registry are not the bytes the integrity field describes — stop and investigate.
## 3. Verify registry signatures and provenance for your install tree
npm audit signatures
## 4. Read the attestation the registry stored for that version
curl -s https://registry.npmjs.org/-/npm/v1/attestations/<pkg>@<version> \
| jq '.attestations[].predicateType'
Expect https://slsa.dev/provenance/v1. The provenance payload names the source repository, the workflow file, and the commit, and includes a subject digest you can compare against the tarball you downloaded.
## 5. Find action references that are not SHA-pinned
grep -rn "uses:" .github/workflows/ | grep -vE "@[0-9a-f]{40}"
## 6. Review history on the files that can publish
git log --oneline -- .github/workflows/
Command 6 is the one I'd run first after any supply-chain incident touching a repo I maintain. Publish-step edits that did not come from the release process are the highest-signal artifact available.
What weak signals look like in logs
These are heuristics with false positives, not detections:
- A publish whose provenance workflow ref does not match the default branch head of the repository.
- A version bump with no corresponding reviewed commit on the release branch.
- A new
postinstall,prepare, orprepublishOnlyscript appearing in a patch release. - OIDC or
id-tokenclaims in job logs for a job that has no business publishing. - A
publishjob that ran on apull_requestevent rather than a tag or release.
Legitimate infrastructure changes trip these constantly. Treat them as reasons to look, not reasons to conclude.
Hardening GitHub Actions CI/CD against trusted-publishing abuse
In the order I'd actually do it:
- Pin every action to a full 40-character commit SHA. Use Dependabot to keep the pins moving.
- Set
permissions: {}at workflow level and grantid-token: writeonly on the publish job. - Move publishing into its own workflow triggered by tags or releases, gated behind a protected GitHub Environment with required reviewers.
- Never let pull-request code execute in a job that can request an OIDC token or read publish secrets.
- Once trusted publishing is verified working, revoke the automation tokens. Run both mechanisms in parallel and you have kept the weaker one.
| Control | What it blocks |
|---|---|
| SHA-pinned actions | Mutable-tag takeover of your job's code |
Job-scoped id-token: write | Token minting from unrelated steps and jobs |
| Separate publish workflow | PR-triggered paths reaching the publish step |
| Protected environment | Silent publishes from a compromised branch state |
| Token revocation | Reuse of any credential that leaks anyway |
What this hardening does not fix
A compromised maintainer account still reaches the publish path, because the account can edit the workflow. Environment reviewers are a human control and can be social-engineered with a plausible release request. Provenance proves origin, not intent — a poisoned release published correctly by your own workflow will carry a valid attestation. And none of it protects consumers who install with --ignore-scripts off and never verify a signature.
My position: OIDC removed the long-lived secret, not the trust problem
Trusted publishing is a genuine improvement. Static npm tokens were exfiltratable, long-lived, and usually over-scoped, and provenance gives consumers something checkable that did not exist before. I wouldn't go back.
But framing this story as an npm vulnerability is wrong, and the framing matters because it points the fix in the wrong direction. npm's mechanism did exactly what it was designed to do: it verified that a request came from a configured repository and workflow, and it published. The registry cannot tell whether the workflow that published was the one you intended to run. That is not a registry bug; it is the boundary the design draws.
The design concentrates risk into two places: the CI configuration and the maintainer's GitHub identity. If I had to fix one thing first, it would be SHA-pinning and pulling publish out of any workflow reachable by pull requests. Everything else — environments, reviewers, provenance plumbing — matters after that.
Further Reading
- npm docs: Trusted publishing for npm packages — https://docs.npmjs.com/trusted-publishers
- npm docs: Generating provenance statements — https://docs.npmjs.com/generating-provenance-statements
- GitHub docs: About security hardening with OpenID Connect — https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
- GitHub docs: Security hardening for GitHub Actions — https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
- SLSA specification: Provenance v1.0 — https://slsa.dev/spec/v1.0/provenance
- News report: GHAPPIER supply chain attack compromises 65 GitHub repositories and poisons npm package, CyberSecurityNews, 2026-09-22 — https://news.google.com/rss/articles/CBMia0FVX3lxTE92cEF2M0trN3ZTMWxOYjB2d2lLYnozOEs4QmZwaEZxVjRfandmaXpxd3FKX002MDJpUkJkdEpsLXgyV0RCcXk4VDIzNEx3cXM0Zk9TVzVKX0V4cmVkLUpVck02V0ZLTWZ5N29v0gFwQVVfeXFMTl9xWGhDVVgxVFVocHdkN0JBSXoyLUlxMFBmT0YybWw3MGtNZUx3NE56V3Y5YzBKV2N3eUI3WEIyZlpRMWI2aC1JR2FQZHZKNUhFcFBraFpUNGVhVnBDU1BCVEltakpMcGgwSzBPNklOVw
- News report: New malware loader GHAPPIER abused npm trusted publishing, MSSP Alert, 2026-09-22 — https://news.google.com/rss/articles/CBMikwFBVV95cUxQUVp6NGM0WkJIS1NJdWFWa2I1LXJmdDBPUHN2N2c0TkJ5TEVOZTVsOGZaTV9ZenZGT2VfWHl5RGI5RVZoTV82Q2w1b09SLTZELUNrc3ozVnZ4NTVseE1yaGVMN2RZUE5qZlRGY2tmYnRaNndfMXBnbmFPSmtsQVdwX0lmdDdnYUNtVFFoUTZiTXRtOHM
- News report: Shai-Hulud attack nips cyber-firm CrowdSec's GitHub data, Dark Reading, 2026-09-22 — https://news.google.com/rss/articles/CBMipwFBVV95cUxQMmVnTUFpMzgxVGtUUkZDaUtIdTlBWEc2SXFjZVc2WVJRUkU3bmVURllWYlhvc2xjc21sZE5tWXd5WldrcW5PZ0xXUy1hQUY3Zk5nYlEya2dNeUJoekpWQzM0VE41Ulg2UGtNYnltRklPWkhOb214MU9Da2xQZ3hNcVFYdHpxTUtzQTJUUkNRaXFWZ2JvZkhLS2dNWGhvcHU1YU5LaEwyVQ


