The evasion that beat everyone,
and how we closed the class.
On June 26, 2026, the Miasma Mini Shai-Hulud worm reached four trusted Backstage plugins from
ImmobiliareLabs, the engineering org behind Italy's largest property portal. The payload ran at
npm install through a binding.gyp trick that carries no lifecycle
script, so the usual signal had nothing to fire on. Here is the timeline, the miss, and the fix.
-
Jun 24 · 15:39:06 UTC
It begins in a trusted release Action
The attacker force-pushes an imposter commit into
codfish/semantic-release-actionand repoints seven version tags (v1.6.1 through v2.2.1) at it. The Action is quietly rewritten from Docker to a composite one, with two steps guarded byif: always()that run a ~512 KB Bun payload to steal OIDC tokens and PATs. This is the class our commit-level watcher reads, a force-push that rewrites a workflow to run a bundled payload. -
Jun 24 · the lock-in
Tags frozen, then the worm spreads
The attacker uses GitHub Repository Rulesets to make the hijacked tags immutable, including the rolling
v5, so a downstream pin cannot be cleaned by a tag reset. With the stolen tokens it injects steps into other repos' workflows and republishes trojanized versions, the upstream access path into the @immobiliarelabs publish pipeline. -
Jun 26 · the burst
22 versions in 30 seconds · in our data within a minute
Malicious patch releases land on every major line of four plugins at once:
backstage-plugin-gitlab,-gitlab-backend,-ldap-authand-ldap-auth-backend. Our npm firehose ingests every one within about a minute of publication. Seen, recorded, timestamped. -
the evasion
No preinstall. No postinstall. The payload hides in
binding.gypEvery malicious version declares no
install,preinstallorpostinstallscript. Insteadbinding.gypcarries"<!(node index.js > /dev/null 2>&1 && echo stub.c)". node-gyp evaluates that command-expansion duringnpm install, so a 5 MB obfuscatedindex.jsexecutes with no lifecycle hook anywhere to be seen. -
the miss
Why everyone missed it, us included
At first the versions were not flagged, for the same reason the rest of the industry missed them: install-time detection across the ecosystem was keyed on lifecycle scripts, and there are none here. We are telling this plainly because the response matters more than the miss. The right answer to a missed technique is not a one-off rule. It is to close the class.
-
the payload
Credential harvest, then persistence through your AI assistant
The loader peels a ROT-2 Caesar shift, then AES-128-GCM, then an obfuscator.io layer, fetches
Bun v1.3.13to run the next stage, and harvests.envfiles, cloud and registry tokens, Vault, Kubernetes and SSH secrets. TheninfectHostwrites aSessionStarthook into.claude/settings.json, rules into.cursor/rules/, a.github/copilot-instructions.mdand afolderOpentask into.vscode/tasks.json, so the developer's own coding assistant re-runs the payload. -
the fix · we close the class
Read
binding.gypas install-time execution · new signals fireWe extended the engine to treat a
binding.gyp<!( )command-expansion that runs a bundled script (or chains and redirects) as install-time code execution, the same severity class as a malicious postinstall, and to flag package code that writes to AI-assistant config paths. Not one rule for one package. The whole evasion class, and the persistence class, shut at once. -
the verdict
Run against the live packages: BLOCK
With the class closed, the poisoned plugins score in the block band and every surface agrees:
npm installthrough the proxy returns403, CI gates fail, the editor flags it, the CLI exits non-zero. Thebinding.gyppayload never reaches disk, and the AI-assistant configs are never rewritten. -
forward · every install, every customer
The class stays shut
The same check now runs on every npm publish we ingest, so the next binding.gyp payload is caught on the first pass, not the second. And when a malicious version is unpublished within the day, the classic publish-then-vanish evasion, we cluster the scope's rapid-unpublish burst from a lightweight gone-marker even when the tarball is already gone. Campaign attributed to Miasma Mini Shai-Hulud by Socket, StepSecurity and ReversingLabs.
The attack started a layer up,
in a trusted release Action.
Two days before the burst, the whole thing began as a force-pushed commit that rewrote codfish/semantic-release-action. This is the change, reconstructed from the public forensic record, and the class our commit-level watcher reads.
if: always() step running a bundled payload. The workflow-injection and orphan-tag detectors corroborate the same event.fig. 01 · commit-level diff · the Action rewrite, reconstructed from the public forensic record (StepSecurity, ReversingLabs)
Not an advisory after the fact.
The verdict.
Once the class was closed, every CyberXYZ proxy returns the same answer for these versions, block, before the tarball can run its binding.gyp payload.
fig. 02 · proxy verdict · the answer every CyberXYZ proxy now returns for these versions, at install
Blocked install: @immobiliarelabs/backstage-plugin-gitlab@7.0.2 · npm
A binding.gyp command-expansion <!(node index.js …) executes at node-gyp build with no preinstall or postinstall hook. Flagged as gyp_command_execution, install-hook context.
Package code writes to .claude/settings.json, .cursor/rules/, .github/copilot-instructions.md and .vscode/tasks.json, reinfecting through the developer's AI coding tools.
The staged payload reads .env files and npm / GitHub / AWS / Azure / GCP / Vault / registry tokens, SSH keys and Kubernetes configs, a CI/CD-targeted harvester.
22 versions from one scope share an identical binding.gyp fingerprint in a 30-second window, a coordinated wave, not a one-off. Campaign: Miasma Mini Shai-Hulud.
fig. 03 · install verdict · the XYZ score and the four signals that fire once the class is closed
If any of these are in a lockfile,
treat the host as compromised.
The malicious versions were unpublished, but a cached tarball or a pinned lockfile can still carry them. Pull these, rotate every credential the runner could reach, and check the AI-assistant configs below.
# affected @immobiliarelabs versions (npm)
backstage-plugin-gitlab 1.0.1 2.1.2 3.0.3 4.0.2 5.2.1 6.13.1 7.0.2
backstage-plugin-gitlab-backend 3.0.3 4.0.2 5.2.1 6.13.1 7.0.2
backstage-plugin-ldap-auth 1.1.4 2.0.5 3.0.2 4.3.2 5.2.1
backstage-plugin-ldap-auth-backend 1.1.3 2.0.5 3.0.2 4.3.2 5.2.1
# indicators of compromise
binding.gyp sha256 ef641e956f91d501b748085996303c96a64d67f63bfeef0dda175e5aa19cca90
campaign marker "thebeautifulsnadsoftime"
payload runtime bun v1.3.13 (github.com/oven-sh/bun/releases)
persistence paths .claude/settings.json .cursor/rules/ .github/copilot-instructions.md .vscode/tasks.json
Full technique breakdown, IOC list and remediation steps in the CyberXYZ blog.
Sources: CyberXYZ threat intelligence and the CyberXYZ technical write-up, corroborated by Socket, StepSecurity, ReversingLabs and The Hacker News (Jun 2026). How the engine decides: the methodology.