Skip to content
Merged
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 @@ -25,6 +25,8 @@ The classic npm route is to publish a malicious package version with `preinstall
}
```

ChainDrop kept legitimate package functionality and added a small `setup.mjs` loader for a larger obfuscated payload. Its loader bootstrapped Bun when needed, used an environment marker to avoid recursive execution, detached on workstations, and ran inline in CI, leaving its debug output in the workflow log.<sup>[[13]](#references)</sup>

Defenders often monitor these scripts, so red-team reviews should also inspect less obvious execution paths.<sup>[[11]](#references)</sup>

### `binding.gyp` / node-gyp execution (Phantom Gyp)
Expand All @@ -37,15 +39,48 @@ Practical checks:
- Treat a sudden `binding.gyp` addition as an execution primitive, especially if defenders rely on lifecycle-hook monitoring or `--ignore-scripts`.<sup>[[6]](#references)[[10]](#references)[[11]](#references)</sup>
- Review release jobs that run `npm install`, `npm rebuild`, or dependency build steps after restoring untrusted artifacts/caches.<sup>[[2]](#references)[[11]](#references)</sup>

## Runner Secret Theft After Install-Time Execution

On a Linux GitHub Actions runner, install-time malware can look beyond environment variables and temporary files. Where the runner user, procfs mount options, and ptrace policy permit access, it can locate `Runner.Worker`, inspect `/proc/<pid>/maps`, and read `/proc/<pid>/mem` for short-lived OIDC tokens or workflow secrets that were never written to disk.<sup>[[13]](#references)</sup>

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

Memory scraping can expose short-lived secrets that never reach disk and may disappear when the job ends.<sup>[[13]](#references)</sup>

## Workflow-Assisted Exfiltration

With a write-capable GitHub identity, an attacker can add a workflow that serializes the `secrets` context and stores it as a workflow artifact. This can avoid a direct connection to an external C2 when GitHub traffic remains allowed.<sup>[[13]](#references)[[14]](#references)</sup>

```yaml
steps:
- name: Stage secrets
env:
ALL_SECRETS: ${{ toJSON(secrets) }}
run: printf '%s' "$ALL_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)` or upload artifacts.<sup>[[13]](#references)[[14]](#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:<sup>[[1]](#references)</sup>

1. Harvest maintainer secrets (`~/.npmrc`, PATs, OIDC request env vars, cloud creds, SSH keys).<sup>[[1]](#references)</sup>
2. Enumerate packages the compromised identity or team can publish to.<sup>[[1]](#references)[[7]](#references)</sup>
2. Enumerate packages the compromised identity or team can publish to.<sup>[[1]](#references)[[4]](#references)[[7]](#references)[[12]](#references)</sup>
3. Republish malicious versions across each writable package.<sup>[[1]](#references)</sup>
4. Let downstream installs create more credential-generation nodes.<sup>[[1]](#references)</sup>

ChainDrop downloaded or reconstructed each writable package while preserving its expected functionality, added the loader to `preinstall`, bumped the patch version, and republished it so later installs generated more victims.<sup>[[13]](#references)</sup>

Useful enumeration from a compromised npm identity:<sup>[[7]](#references)</sup>

```bash
Expand All @@ -58,26 +93,31 @@ Attackers usually prefer packages with frequent CI installs, transitive populari

## 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 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.<sup>[[1]](#references)[[2]](#references)[[8]](#references)</sup>
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 already authorized as an npm trusted publisher and granted `id-token: write`, the job can mint a GitHub OIDC token and exchange it for an npm publishing credential; the malicious release can also receive valid provenance because the legitimate workflow built and published it.<sup>[[1]](#references)[[2]](#references)[[8]](#references)[[13]](#references)[[15]](#references)</sup>

Provenance answers **which workflow built this artifact**, not **whether the workflow, source tree, cache, or build steps were clean**.<sup>[[1]](#references)[[2]](#references)[[13]](#references)[[15]](#references)</sup>

Provenance answers **which workflow built this artifact**, not **whether the workflow, source tree, cache, or build steps were clean**.<sup>[[1]](#references)[[2]](#references)</sup>
ChainDrop implemented a repository-gated path that injected a Git-pinned `@opensearch/setup` dependency imitating the legitimate `@opensearch-project` scope. Review dependency diffs even when lifecycle scripts do not change.<sup>[[13]](#references)</sup>

High-signal review points:

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

## Hardening

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

## References

Expand All @@ -93,5 +133,8 @@ High-signal review points:
- [10] [npm-install | npm Docs](https://docs.npmjs.com/cli/install/)
- [11] [Miasma npm Supply Chain Attack: Self-Spreading Worm via Phantom Gyp | StepSecurity](https://www.stepsecurity.io/blog/binding-gyp-npm-supply-chain-attack-spreads-like-worm)
- [12] [Organizations | npm Docs](https://docs.npmjs.com/cli/v11/using-npm/orgs/)
- [13] [ChainDrop: Inside a Self-Propagating npm Worm](https://unit42.paloaltonetworks.com/chaindrop-npm-worm-analysis/)
- [14] [Store and share data with workflow artifacts - GitHub Docs](https://docs.github.com/en/actions/tutorials/store-and-share-data)
- [15] [Generating provenance statements | npm Docs](https://docs.npmjs.com/generating-provenance-statements/)

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