Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

## Overview

After an attacker gets code execution in a GitHub Actions release workflow, maintainer workstation, or package build pipeline, npm publishing becomes a high-impact pivot. The goal is usually to steal publisher identity material, publish malicious versions, and turn downstream installs into more credential-generation nodes.
After an attacker gets code execution in a GitHub Actions release workflow, maintainer workstation, or package build pipeline, npm publishing becomes a high-impact pivot. The goal is usually to steal publisher identity material, publish malicious versions, and turn downstream installs into more credential-generation nodes.<sup>[[6]](#references)</sup>

Typical credential sources:

- `~/.npmrc`, `NPM_TOKEN`, registry sessions, and npm automation tokens.
- GitHub PATs, `GITHUB_TOKEN`, release-bot credentials, SSH keys, and `.netrc` / git credential helpers.
- GitHub Actions OIDC request material (`ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`) in jobs with `id-token: write`.
- GitHub Actions OIDC request material (`ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`) in jobs with `id-token: write`.<sup>[[7]](#references)</sup>
- Cloud credentials, Vault tokens, Kubernetes service account tokens, and `.env` files present in the release environment.

## Install-Time Execution Primitives
Expand All @@ -27,26 +27,56 @@ The classic npm route is to publish a malicious package version with `preinstall
}
```

Modern npm worms usually keep the legitimate package code intact and only add a small loader plus a large obfuscated payload. A practical pattern is to bootstrap a portable runtime such as Bun if it is missing, set an environment marker to avoid recursive relaunch, detach on workstations so the install completes quietly, and stay inline in CI so the payload inherits workflow credentials.<sup>[[6]](#references)</sup>

Defenders often monitor these scripts, so red-team reviews should also inspect less obvious execution paths.

### `binding.gyp` / node-gyp execution (Phantom Gyp)

Not every install-time execution path lives in `package.json` lifecycle hooks. `node-gyp`'s configure step looks for a `binding.gyp` file in the package directory, so a compromised publisher can shift execution into the native build path and bypass controls that only audit `preinstall` / `postinstall`.
Not every install-time execution path lives in `package.json` lifecycle hooks. `node-gyp`'s configure step looks for a `binding.gyp` file in the package directory, so a compromised publisher can shift execution into the native build path and bypass controls that only audit `preinstall` / `postinstall`.<sup>[[5]](#references)</sup>

Practical checks:

- Inspect the **published tarball**, not only the Git repo, for unexpected `binding.gyp`, `node-gyp`, or native-addon metadata in packages that should be pure JavaScript.
- Treat a sudden `binding.gyp` addition as an execution primitive, especially if defenders rely on lifecycle-hook monitoring or `--ignore-scripts`.
- Review release jobs that run `npm install`, `npm rebuild`, or dependency build steps after restoring untrusted artifacts/caches.

## Runner Secret Theft After Install-Time Execution

If the malicious install code lands on a Linux GitHub Actions runner, do not limit secret hunting to environment variables and temporary files. A more robust pivot is to locate the privileged `Runner.Worker` process, inspect `/proc/<pid>/maps`, and read `/proc/<pid>/mem` to recover short-lived OIDC tokens and workflow secrets that were never written to disk.<sup>[[6]](#references)</sup>

```bash
PID=$(pgrep -f 'Runner.Worker|runner.worker')
cat /proc/$PID/maps | head
# If procfs / ptrace policy allows it, read /proc/$PID/mem and scan for
# JWTs, gh[pousr]_, cloud creds, or other plaintext secret markers.
```

This matters because GitHub log masking only affects rendered output; it does not protect plaintext already loaded into runner memory.<sup>[[6]](#references)</sup>

## Workflow-Assisted Exfiltration

With a write-capable GitHub identity, the attacker can add a workflow that serializes the full `secrets` context and stores it as an artifact instead of sending it directly to an external C2. Artifacts persist after the workflow finishes, so this is useful when outbound traffic is filtered but GitHub traffic is still allowed.<sup>[[6]](#references)[[8]](#references)</sup>

```yaml
steps:
- run: printf '%s' '${{ toJSON(secrets) }}' > secrets.json
- uses: actions/upload-artifact@v4
with:
name: repo-secrets
path: secrets.json
```

Review unexpected `.github/workflows/*.yml` additions, especially files that call `toJSON(secrets)`, upload artifacts, or only exist in temporary attacker-created branches.<sup>[[6]](#references)</sup>

## Wormable npm Publishing

Once code runs in a maintainer workstation or release workflow, a single stolen registry identity can be turned into self-propagating package compromise:

1. Harvest maintainer secrets (`~/.npmrc`, PATs, OIDC request env vars, cloud creds, SSH keys).
2. Enumerate packages the compromised identity or team can publish to.
3. Republish malicious versions across each writable package.
4. Let downstream installs create more credential-generation nodes.
3. Repack or reconstruct each writable package while preserving its expected functionality.
4. Add a loader such as `setup.mjs`, wire it to `preinstall`, bump the patch version, and republish so downstream installs create more credential-generation nodes.<sup>[[6]](#references)</sup>

Useful enumeration from a compromised npm identity:

Expand All @@ -56,37 +86,47 @@ npm access ls-packages
npm access ls-collaborators <scope-or-package>
```

Attackers usually prefer packages with frequent CI installs, transitive popularity, or release automation that will install the malicious version quickly.
Attackers usually prefer packages with frequent CI installs, transitive popularity, or release automation that will install the malicious version quickly.<sup>[[6]](#references)</sup>

## Trusted Publishing, Typosquats, and Provenance Limits

## Trusted Publishing and Provenance Limits
Trusted publishing/OIDC removes long-lived static npm tokens, but it does not make a compromised release workflow safe. If attacker-controlled code runs inside a workflow that is already authorized as an npm trusted publisher and has `id-token: write`, the job can mint a GitHub OIDC token and exchange it for a real npm publishing credential without ever stealing a reusable npm token.<sup>[[2]](#references)[[6]](#references)[[7]](#references)</sup>

Trusted publishing/OIDC removes long-lived static npm tokens, but it does not make a compromised release workflow safe. If the attacker controls code that runs in a job with `id-token: write`, the malicious release can still receive valid provenance because the legitimate workflow really built and published it.
A quieter variant is to avoid lifecycle hooks entirely and inject a new dependency whose name looks like an internal helper or a near-match for the legitimate package scope, sometimes pinned to a specific Git commit. This evades detections that only diff `scripts` or search for `preinstall` / `postinstall` changes.<sup>[[6]](#references)</sup>

Provenance answers **which workflow built this artifact**, not **whether the workflow, source tree, cache, or build steps were clean**.
Trusted publishing can also produce valid provenance for a malicious package. npm provenance and Sigstore-style attestations prove which workflow produced the artifact; they do **not** prove that the workflow, caches, or build inputs were clean. A compromised but legitimate release workflow can therefore publish a backdoored package with cryptographically valid provenance.<sup>[[6]](#references)[[7]](#references)[[9]](#references)</sup>

High-signal review points:
High-signal review points for review workflows, tarball diffs, and provenance verification:<sup>[[6]](#references)[[7]](#references)[[9]](#references)</sup>

- Workflows combining `id-token: write` with `npm publish`, `pnpm publish`, `changesets`, release bots, or custom publish wrappers.
- Release jobs that restore caches or artifacts from lower-trust workflows before publishing.
- Jobs that publish without human approval, environment protection rules, or a second reviewer.
- Workflows that request OIDC before all build inputs have been verified.
- Package diffs that add a dependency imitating the maintainer's own scope or an internal helper name.
- Provenance that verifies correctly but points to a workflow run or workflow file that executed attacker-controlled code.

## Hardening

Useful defensive controls include the following:<sup>[[2]](#references)[[3]](#references)[[6]](#references)[[9]](#references)</sup>

- Use trusted publishing/OIDC instead of static npm tokens, but pair it with protected environments and human approval for sensitive scopes.
- Add staged publishing / human 2FA approval for high-impact packages where possible.
- Use `minimumReleaseAge` or equivalent dependency quarantine controls before consuming newly published package versions.
- Separate cache keys by trust boundary and never execute restored cache contents before integrity checks.
- Diff published tarballs against source repositories, and alert on unexpected native build metadata such as `binding.gyp`.
- Diff published tarballs against source repositories, and alert on unexpected native build metadata such as `binding.gyp`, unexpected new dependencies, or new top-level loaders.
- Disable or tightly review lifecycle scripts in CI (`npm config set ignore-scripts true`) where builds do not need them.
- Monitor package access (`npm access ls-packages`) and remove stale maintainers, bots, and teams.
- Treat new workflow files plus artifact uploads as part of the package-publishing attack surface, not only as GitHub-side persistence.

## References

- [What the Miasma campaign reveals about the new supply chain threat model and the underground market for developer credentials](https://www.tenable.com/blog/what-the-miasma-campaign-reveals-about-the-new-supply-chain-threat-model-and-the-underground)
- [Trusted publishing for npm packages | npm Docs](https://docs.npmjs.com/trusted-publishers/)
- [Staged publishing for npm packages | npm Docs](https://docs.npmjs.com/staged-publishing/)
- [npm orgs | npm Docs](https://docs.npmjs.com/using-npm/orgs.html)
- [node-gyp README](https://github.com/nodejs/node-gyp)
- [1] [What the Miasma campaign reveals about the new supply chain threat model and the underground market for developer credentials](https://www.tenable.com/blog/what-the-miasma-campaign-reveals-about-the-new-supply-chain-threat-model-and-the-underground)
- [2] [Trusted publishing for npm packages | npm Docs](https://docs.npmjs.com/trusted-publishers/)
- [3] [Staged publishing for npm packages | npm Docs](https://docs.npmjs.com/staged-publishing/)
- [4] [npm orgs | npm Docs](https://docs.npmjs.com/using-npm/orgs.html)
- [5] [node-gyp README](https://github.com/nodejs/node-gyp)
- [6] [ChainDrop: Inside a Self-Propagating npm Worm](https://unit42.paloaltonetworks.com/chaindrop-npm-worm-analysis/)
- [7] [OpenID Connect reference - GitHub Docs](https://docs.github.com/en/actions/reference/security/oidc)
- [8] [Store and share data with workflow artifacts - GitHub Docs](https://docs.github.com/en/actions/tutorials/store-and-share-data)
- [9] [Generating provenance statements | npm Docs](https://docs.npmjs.com/generating-provenance-statements/)

{{#include ../../../banners/hacktricks-training.md}}