← Back to Attack Research

When the IDE is the initial access: trojanized VS Code / Nx Console extensions

A developer accepts an extension auto-update. Buried in the bundle is a credential stealer that walks .npmrc, .git-credentials, the AWS profile, and SSH keys, then phones home. CVE-2026-48027 turned the Nx Console extension into initial access for an entire CI estate. Here is the attacker decision tree from one malicious install to GitHub, cloud, and pipeline compromise.

Your most privileged endpoint is not a server. It is a laptop running an IDE that auto-updates extensions, holds a live GitHub token, an AWS profile, an npm publish credential, and an SSH key to production. On 18 May 2026 the Nx Console extension, a popular VS Code monorepo helper, shipped a trojanized release, CVE-2026-48027, part of the same "TeamPCP" campaign that produced the Mini Shai-Hulud self-replicating npm worm. The payload walked the developer's home directory for secrets, exfiltrated them to attacker infrastructure, and the harvested GitHub, npm, and cloud tokens then became initial access into the CI pipeline. This piece walks the attacker decision tree from one auto-accepted extension update to a fully compromised software factory, and the controls that break the chain.

For fifteen years the supply-chain conversation has been about dependencies: the packages your code imports at build time. That is half the surface. The other half is the tooling your engineers install to write the code in the first place: the language servers, the linters, the formatters, the monorepo helpers, the AI coding assistants. Every one of those is an executable that runs with your developer's full local privilege, on the machine that holds every credential your developer uses to ship software. The IDE extension marketplace is a package registry with the same trust model as npm or PyPI, except the install target is not a sandboxed build container. It is the workstation at the center of your engineering org.

This piece walks the trojanized-extension attack the same way our JWT and SSRF pieces walked their classes: the pattern in one paragraph, why it ships, the attacker's full decision tree, a composite real-world scenario grounded in a real campaign, and the Find / Prove / Fix / Verify contract that closes it. Verifiable security.

The attack pattern in one paragraph

A developer installs, or auto-updates, an IDE extension. The extension is a signed, marketplace-hosted package, so it sails through whatever trust gate the marketplace applies (which, for VS Code's Open VSX and the official Marketplace, is largely automated malware scanning, not human review). The extension's activation code runs in the IDE's extension host with the user's full OS privilege the moment a matching file type is opened or a matching command fires. The malicious payload does not exploit a memory bug; it simply reads files: ~/.npmrc, ~/.git-credentials, ~/.config/gh/hosts.yml, ~/.aws/credentials, ~/.config/gcloud, ~/.ssh/id_*, ~/.kube/config, environment variables, and the in-memory tokens the IDE itself caches for its GitHub and cloud integrations. It bundles them, POSTs them to an attacker endpoint (often disguised as telemetry), and exits. The developer sees nothing. The harvested credentials are then replayed against GitHub, the npm registry, the cloud control plane, and the CI/CD system, turning one trojanized install into the keys to the entire software factory. In the TeamPCP/Mini Shai-Hulud variant, the stealer also self-propagated: it used the harvested npm token to publish poisoned versions of the developer's own packages, so every downstream consumer became a new infection.

The reason this is initial access and not just "one stolen laptop" is the blast radius of the credentials a developer machine holds. A single engineer routinely carries: push access to dozens of repos, an npm token with publish rights, a long-lived cloud key, and a GitHub Actions context that can mint OIDC tokens. The extension does not need to escalate. The developer already is privileged. The extension just collects.

Why this still ships in 2026

If installing untrusted code on a privileged machine is obviously dangerous, why did a trojanized Nx Console reach real developers in May 2026? Four structural reasons:

  1. The marketplace trust model is automated, not curated. Both the official VS Code Marketplace and Open VSX scan for known-bad signatures and run heuristics, but a credential stealer that only reads dotfiles and POSTs JSON looks almost identical to legitimate telemetry. There is no human in the loop for the long tail of extensions, and a popular extension's namespace is a high-value target precisely because the scanning is shallow.
  2. Auto-update is the default and it is silent. VS Code updates extensions in the background by default. A developer who installed a clean version of an extension months ago gets the trojanized version pushed to them with no prompt, no diff, and no notification beyond a changelog they will never read. The trust decision was made once, at install time, and is never revisited.
  3. The compromise is upstream of the developer. In the TeamPCP campaign the attackers did not write a fake extension; they compromised the publishing credentials of a legitimate one (and, in the npm dimension, stole maintainer tokens via the same stealer, then republished). The extension your developer trusts is the extension that attacks them. Reputation and download count, the signals everyone uses to judge safety, are exactly the signals the attacker hijacks.
  4. Developer machines are the least-monitored privileged endpoints in the org. The production fleet has EDR, egress filtering, and least-privilege IAM. The laptop has a personal GitHub token, a cloud admin key "for convenience," and outbound internet to anywhere. Security spends its budget on the servers; the attacker spends their effort on the laptops.

The attacker decision tree

ATTACKER DECISION TREE Trojanized IDE Extension → CI ┌───────────────────────────────────────────┐ │ 1. Get code onto a dev machine │ │ a) compromise a popular extension's │ │ publisher token → push trojan ver │ │ b) typosquat / namespace-hijack on │ │ Open VSX or the Marketplace │ │ c) ride an existing npm-worm infection │ │ (Mini Shai-Hulud) into the IDE │ └───────────────────┬───────────────────────┘ │ (auto-update; no prompt) ▼ ┌───────────────────────────────────────────┐ │ 2. Activate + harvest local secrets │ │ ~/.npmrc ~/.git-credentials │ │ ~/.aws/creds ~/.config/gcloud │ │ ~/.config/gh ~/.ssh/id_* │ │ ~/.kube/config process env vars │ │ IDE-cached GitHub / cloud OAuth tokens │ └───────────────────┬───────────────────────┘ │ (POST as "telemetry") ▼ ┌───────────────────────────────────────────┐ │ 3. Triage the loot │ │ - which token has repo:write / admin? │ │ - npm publish rights? │ │ - cloud key scope (sts get-caller-id)? │ │ - SSH to which hosts (known_hosts)? │ └───────────────────┬───────────────────────┘ ┌───────┼─────────────┬────────────┐ ▼ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌─────────┐ ┌──────────┐ │ GitHub │ │ npm │ │ Cloud │ │ CI │ │ push │ │ publish│ │ control │ │ pipeline │ │ secret │ │ poison │ │ plane │ │ takeover │ │ steal │ │ deps │ │ enum │ │ via OIDC │ └───┬────┘ └───┬────┘ └────┬────┘ └────┬─────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌───────────────────────────────────────────┐ │ 4. Persist + propagate │ │ - add a malicious GitHub Action / │ │ reusable workflow to a trusted repo │ │ - publish a poisoned package version │ │ (self-replicating worm dimension) │ │ - plant a cloud IAM backdoor user/role │ │ → every downstream consumer = new host │ └───────────────────────────────────────────┘

From one auto-accepted extension update to GitHub, npm, cloud, and CI, the worm dimension closes the loop back to step 1.

The decision tree's branch point is step 3. The attacker does not know in advance which credential is the crown jewel; they harvest everything and triage afterward. A developer with only read-only cloud access but npm publish rights becomes a worm-propagation node. A developer with a personal access token scoped to repo and workflow becomes a CI-pipeline takeover. The same payload services every branch; the loot decides the path.

A composite real-world scenario

The setting is a mid-sized product company, a forest-products manufacturer with a modern software arm running an Nx monorepo, npm-based front ends, and GitHub Actions for CI, deploying to a cloud account that touches both corporate IT and mill OT integration. SOX-listed, so the release pipeline is audit-material. A platform engineer uses VS Code with the Nx Console extension to manage the monorepo. On 18 May 2026, between 12:30 and 12:48 UTC, the trojanized Nx Console v18.95.0 auto-updates on their machine. They open the workspace; the extension activates.

The payload runs in the extension host with the engineer's privileges. It reads the obvious targets:

# What the stealer collects (illustrative, reconstructed from the
# campaign's documented behaviour — read-only file access, no exploit)
~/.npmrc                      # //registry.npmjs.org/:_authToken=npm_xxxx
~/.git-credentials           # https://USER:ghp_xxxx@github.com
~/.config/gh/hosts.yml       # gh CLI oauth_token: gho_xxxx
~/.aws/credentials           # [default] aws_secret_access_key=...
~/.config/gcloud/*.json      # application_default_credentials.json
~/.ssh/id_ed25519            # private key + known_hosts (target list)
process.env                  # CI_*, NPM_TOKEN, GH_TOKEN, AWS_* inherited

It bundles the haul, base64-encodes it, and POSTs it to an endpoint that, in request logs, looks like an analytics beacon. The engineer notices nothing: no crash, no slowdown, no prompt. Total time from activation to exfiltration: under two seconds.

Now the attacker triages. The gho_ token from the gh CLI has repo and workflow scope. The npm_ token has publish rights on three internal-but-public packages. The AWS key, when run through aws sts get-caller-identity, belongs to a role with iam:* on a CI service account. Three separate paths open at once. The attacker picks the highest-leverage one first: the CI pipeline.

# Using the stolen workflow-scoped GitHub token, the attacker adds a
# reusable-workflow change to a trusted internal repo. The classic
# pull_request_target + untrusted-checkout pattern lets the injected
# step run with repo secrets in scope.
on:
  pull_request_target:        # runs with the BASE repo's secrets
jobs:
  build:
    steps:
      - uses: actions/checkout@v4
        with: { ref: ${{ github.event.pull_request.head.sha }} }   # untrusted code
      - run: ./scripts/build.sh   # now executes attacker-controlled script
        env:
          AWS_ROLE: ${{ secrets.DEPLOY_ROLE_ARN }}   # exfiltrated via OIDC

The injected step requests a GitHub Actions OIDC token and exchanges it for cloud credentials, the same trusted path the legitimate deploy uses, now serving the attacker. Simultaneously, the stolen npm token publishes a poisoned patch version of one of the company's own packages. Every downstream service that runs npm install within its float range pulls the poisoned version, re-runs the stealer in their CI, and the worm dimension closes the loop: new tokens, new repos, new propagation. One auto-accepted extension update has become a self-replicating compromise of the software factory.

The reason it works end-to-end: at no point did the attacker break a cryptographic control or exploit a memory-corruption bug. They rode trust: the developer's trust in the marketplace, the CI's trust in the developer's token, the registry's trust in the maintainer's publish credential, and the downstream consumers' trust in the package they always pull. Trust is the attack surface.

What we observe in customer environments

We are honest about the limits of our visibility. CelvexGroup's continuous validation runs against assets a customer scopes in; we do not have an agent on every developer laptop, and we say so. What we can and do assess externally and through scoped supply-chain review is the blast-radius surface that makes this campaign devastating: the CI/CD configuration, the package-publish trust model, and the secret-exposure paths. Across engagements over the past nine months, the recurring findings are:

The honest read: the trojanized-extension entry is hard for us to see from outside, but the blast radius that turns it into a factory-wide compromise is squarely in scope, frequently exposed, and directly fixable.

What to do about it: the developer-estate contract

You cannot make an IDE extension trustworthy by inspecting it. You break the chain by shrinking what a compromised developer machine can reach. The controls below are ordered by leverage: the first three would have contained the scenario above even after the stealer ran.

Developer-estate supply-chain contract: controls that break the chain

You cannot inspect an extension into being trustworthy. You break the chain by making a compromised developer machine worthless: short-lived credentials, locked-down CI, egress you can see.

The audit, in concrete terms, starts with two greps and one policy review:

# Find the CI-takeover primitive across your org's workflows
$ grep -rEl "pull_request_target" .github/workflows/ /dev/null

# Find long-lived publish/cloud tokens baked into CI
$ grep -rE "NPM_TOKEN|AWS_ACCESS_KEY_ID|_authToken" .github/ ci/ 2>/dev/null

# Then: VS Code policy — confirm allowed extensions are pinned and
# auto-update is disabled on managed machines (MDM / settings policy):
#   "extensions.autoUpdate": false
#   "extensions.allowed": { ".": "" }

Read each match. Confirm no untrusted checkout runs under elevated secrets. Confirm every standing token can be replaced with a short-lived equivalent. The exercise is finishable in a day for most engineering orgs, and it converts a factory-wide compromise into a contained, recoverable laptop incident.

How Celvex catches this

Find. Prove. Fix. Verify.

Find

We enumerate the customer's CI/CD and package-publish trust model in scope: reusable-workflow triggers, untrusted-checkout patterns, publish-token longevity, and developer-credential scope: the blast-radius surface a stolen IDE token weaponises.

Prove

For a confirmed CI-takeover primitive we ship a Proof Capsule with the exact workflow file, the trigger that runs untrusted code with secrets in scope, and a non-destructive demonstration against the customer's own staging.

Fix

The Capsule's remediation block points at the developer-estate contract scoped to the finding: which workflow to change, which token to move to OIDC, which credential to shorten, which extension policy to enforce.

Verify

After the fix lands, the re-test confirms the untrusted-checkout path no longer reaches secrets and the standing token is gone. The finding closes automatically and the verified-fix event is recorded for the auditor.

Where we sit on the autonomy curve: at L1.5 today, our supply-chain corpus fingerprints the CI-takeover primitives, publish-token longevity, and known-malicious version sets (including the Mini Shai-Hulud / TeamPCP windows) against a customer's resolved build graph, version-matched, not banner-matched, per our no-false-positive rule. At L2 within 90 days, the corpus correlates a customer's developer-credential scope with the specific repos and cloud roles a single harvested token would reach, producing a blast-radius map per engineer. At L3 within twelve months, the scanner mutates the trust-graph traversal to fit unfamiliar CI topologies and private registries discovered in customer environments. We do not claim L3 today. We do claim that L1.5 reliably catches the blast-radius surface that turns one trojanized extension into a factory-wide event, and ships a reproducible Capsule for each.

Bottom line

The IDE is now an initial-access vector, and the marketplace is a package registry with workstation-level privilege and automated-only review. CVE-2026-48027 and the TeamPCP/Mini Shai-Hulud campaign proved the model end-to-end: one auto-accepted extension update harvested a developer's credentials, and those credentials, not any further exploit, became GitHub, npm, cloud, and CI compromise, with a worm dimension that closed the loop. You cannot vet your way out of this by inspecting extensions. You break the chain by making the developer machine a poor foothold: short-lived credentials, CI that never runs untrusted code under secrets, egress you can watch, and a build graph you version-match against known-malicious sets. That work is finishable in a day, and it is the difference between a contained laptop incident and a self-replicating compromise of everything you ship.

Verifiable security. Find it. Prove it. Fix it. Verify the fix held. That is what we ship.

Sources

Map your developer-estate blast radius.

Free Exposure Check, no signup required. We assess the CI/CD and package-publish trust model that turns one stolen developer token into a factory-wide compromise, and ship a Proof Capsule for the highest-confidence finding.

Run a Free Scan →