critical

CVE

Not assigned

CWE

CWE-506, CWE-494, CWE-829

Affected Surface

`@immobiliarelabs/backstage-plugin-gitlab`, `@immobiliarelabs/backstage-plugin-gitlab-backend`, `@immobiliarelabs/backstage-plugin-ldap-auth`, and `@immobiliarelabs/backstage-plugin-ldap-auth-backend` at the exact malicious versions listed below, Developer workstations, CI runners, and build containers that ran `npm install`, `pnpm install`, or `yarn install` against those versions on or after 26 June 2026, Backstage environments where install-time access exposed GitLab tokens, LDAP-related configuration, npm publish rights, GitHub Actions secrets, cloud credentials, Kubernetes tokens, SSH material, or AI-assistant configuration files

The newest package-compromise story worth adding to Corgea’s /research section is the 26 June 2026 compromise of four @immobiliarelabs Backstage plugin families. This matters because the attacker did not just poison a leaf library. They poisoned packages that are commonly installed inside internal developer portals and CI-backed monorepos, exactly where GitLab tokens, LDAP configuration, cloud credentials, and release automation secrets tend to coexist.

The attack pattern is also important. The malicious versions did not rely on an obvious preinstall or postinstall script in package.json. Instead, they added a binding.gyp file plus a new root index.js, which means shallow checks that only inspect lifecycle-script fields can miss the real execution edge entirely.

Affected packages

Public reporting from Socket, StepSecurity, and the maintainer issue all agree on the same 22 malicious package-version pairs:

PackageMalicious version
@immobiliarelabs/backstage-plugin-gitlab1.0.1
@immobiliarelabs/backstage-plugin-gitlab2.1.2
@immobiliarelabs/backstage-plugin-gitlab3.0.3
@immobiliarelabs/backstage-plugin-gitlab4.0.2
@immobiliarelabs/backstage-plugin-gitlab5.2.1
@immobiliarelabs/backstage-plugin-gitlab6.13.1
@immobiliarelabs/backstage-plugin-gitlab7.0.2
@immobiliarelabs/backstage-plugin-gitlab-backend3.0.3
@immobiliarelabs/backstage-plugin-gitlab-backend4.0.2
@immobiliarelabs/backstage-plugin-gitlab-backend5.2.1
@immobiliarelabs/backstage-plugin-gitlab-backend6.13.1
@immobiliarelabs/backstage-plugin-gitlab-backend7.0.2
@immobiliarelabs/backstage-plugin-ldap-auth1.1.4
@immobiliarelabs/backstage-plugin-ldap-auth2.0.5
@immobiliarelabs/backstage-plugin-ldap-auth3.0.2
@immobiliarelabs/backstage-plugin-ldap-auth4.3.2
@immobiliarelabs/backstage-plugin-ldap-auth5.2.1
@immobiliarelabs/backstage-plugin-ldap-auth-backend1.1.3
@immobiliarelabs/backstage-plugin-ldap-auth-backend2.0.5
@immobiliarelabs/backstage-plugin-ldap-auth-backend3.0.2
@immobiliarelabs/backstage-plugin-ldap-auth-backend4.3.2
@immobiliarelabs/backstage-plugin-ldap-auth-backend5.2.1

If any of those exact versions were installed, treat the installing host as having executed attacker-controlled code.

Why these packages are high-value targets

The affected packages are not generic utility modules. They are Backstage plugins used to connect an internal developer portal to identity and source-control systems:

  • the GitLab packages expose project, pipeline, merge-request, and contributor data inside Backstage
  • the LDAP packages support authentication flows in organizations that still rely on LDAP or Active Directory

That placement changes the blast radius. A successful install-time compromise in a Backstage plugin build often reaches:

  • GitLab tokens and repository metadata
  • internal CI secrets used to build or release the portal
  • cloud credentials available to the build container
  • Kubernetes or Vault material used by platform teams
  • developer-tool configuration files that enable persistence beyond the original install

Independent registry evidence

The registry metadata looks like a coordinated republish wave, not normal maintenance.

For the main GitLab frontend plugin, the package history shows a clean release, a malicious patch release, and then a same-day cleanup release:

@immobiliarelabs/backstage-plugin-gitlab
  2.1.1  2023-01-17T10:36:06.750Z
  2.1.2  2026-06-26T15:01:18.590Z   malicious
  2.1.3  2026-06-26T17:38:32.875Z   clean replacement

  6.13.0 2025-09-08T10:11:50.986Z
  6.13.1 2026-06-26T15:01:38.398Z   malicious
  6.13.2 2026-06-26T17:32:52.634Z   clean replacement

  7.0.1  2026-04-01T11:40:04.322Z
  7.0.2  2026-06-26T15:01:43.052Z   malicious
  7.0.3  2026-06-26T17:24:26.954Z   clean replacement

The current npm dist-tags now point latest to 2.1.3, which is consistent with the maintainer’s emergency cleanup effort. But the malicious versions still matter for incident response because the compromise happened before that cleanup landed.

The tarball shape also changed in a way ordinary patch releases do not:

@immobiliarelabs/backstage-plugin-gitlab@2.1.1
  unpacked size:   228,219 bytes
  file count:      15

@immobiliarelabs/backstage-plugin-gitlab@2.1.2
  unpacked size: 5,220,231 bytes
  file count:      20

A small Backstage plugin does not normally grow by roughly five megabytes and five extra root files for a patch release. StepSecurity’s tarball diff explains the delta: the malicious versions gained a new binding.gyp execution trigger and a new 5 MB root index.js loader that was absent from prior releases.

The vulnerable edge is install time, not runtime

The most important code in this incident is the newly added binding.gyp:

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

That single line is enough to turn npm install into code execution:

npm install @immobiliarelabs/backstage-plugin-gitlab@2.1.2
  -> node-gyp rebuild
  -> /bin/sh -c "node index.js > /dev/null 2>&1 && echo stub.c"
  -> node index.js
  -> decrypt / stage payload
  -> steal secrets and attempt persistence

This is the same Phantom Gyp technique that kept resurfacing across June’s Miasma waves. It matters because many defenses still answer the wrong question:

  • wrong question: “does package.json contain preinstall or postinstall?”
  • right question: “does the published tarball contain any build or startup surface that will execute code during install?”

In the ImmobiliareLabs case, the second question is the one that catches the attack.

The declared entrypoint is not the malicious entrypoint

Socket’s analysis highlights another subtle but important detail: the normal compiled Backstage plugin output under dist/ still looked legitimate enough to mislead casual review. The real malicious execution path sat in the package root:

dist/index.cjs.js     expected library entrypoint
index.js              new 5 MB malicious loader
binding.gyp           new install-time execution trigger

That lets the attacker preserve normal-looking application code while moving execution into a place many teams never inspect.

What the payload does

StepSecurity’s static analysis of @immobiliarelabs/backstage-plugin-gitlab@2.1.2 and Socket’s broader campaign clustering show the same operational pattern seen in adjacent Miasma incidents:

  1. a Caesar-shift wrapper obscures the outer JavaScript layer
  2. AES-128-GCM decrypts embedded blobs
  3. a Bun bootstrap downloads bun from GitHub releases if needed
  4. the main stage runs under Bun rather than Node.js
  5. the payload harvests credentials and attempts follow-on propagation

StepSecurity specifically reports these functional markers in the decoded payload:

infectHost
squatPackage
updateTarball
handleNpmTokens
handlePypiTokens

Those names matter because they move the incident from “credential stealer” to “possible worm operator toolkit.” The public analysis indicates the payload was built to:

  • read GitHub tokens, GitHub App material, and GitHub Actions runtime secrets
  • dump masked secrets from Runner.Worker memory on Linux runners
  • collect AWS, GCP, Azure, Kubernetes, and Vault credentials
  • steal npm, PyPI, RubyGems, and JFrog Artifactory tokens
  • read password-manager and SSH material
  • modify AI-coding-assistant and IDE configuration files for persistence
  • reuse harvested registry credentials to publish additional poisoned artifacts

Why Backstage amplifies the risk

Backstage plugin builds are not normal leaf-package installs. They often happen in repositories that already integrate multiple trust boundaries:

integrations:
  gitlab:
    - host: gitlab.com
      token: ${GITLAB_TOKEN}

and:

'/gitlabci':
  target: '${GITLAB_URL}/api/v4'
  headers:
    PRIVATE-TOKEN: '${GITLAB_TOKEN}'

That configuration pattern, shown in the package’s own setup documentation, explains why a malicious Backstage plugin is high leverage. A compromised install host can sit next to:

  • GitLab API tokens
  • service-catalog build secrets
  • release automation credentials
  • internal portal deployment keys

The attacker never needed a GitLab or LDAP memory-corruption bug. They only needed code to execute in the environment where these integrations were built.

Maintainer response and what it means

The maintainer thread is useful because it confirms two operational facts:

  1. clean patch versions were pushed the same day to steer automated updaters away from the compromised artifacts
  2. at least some malicious frontend GitLab plugin versions were still awaiting npm-side cleanup after the emergency response began

That combination means defenders should not assume “the package is fixed now” ends the investigation. The real question is whether any machine in your environment installed one of the malicious versions before the clean replacements arrived.

Detection and scoping

Start by inventorying the exact affected packages:

npm ls @immobiliarelabs/backstage-plugin-gitlab \
  @immobiliarelabs/backstage-plugin-gitlab-backend \
  @immobiliarelabs/backstage-plugin-ldap-auth \
  @immobiliarelabs/backstage-plugin-ldap-auth-backend

Then search lockfiles for the exact malicious versions:

rg -n \
  "@immobiliarelabs/backstage-plugin-gitlab@(1\\.0\\.1|2\\.1\\.2|3\\.0\\.3|4\\.0\\.2|5\\.2\\.1|6\\.13\\.1|7\\.0\\.2)|@immobiliarelabs/backstage-plugin-gitlab-backend@(3\\.0\\.3|4\\.0\\.2|5\\.2\\.1|6\\.13\\.1|7\\.0\\.2)|@immobiliarelabs/backstage-plugin-ldap-auth@(1\\.1\\.4|2\\.0\\.5|3\\.0\\.2|4\\.3\\.2|5\\.2\\.1)|@immobiliarelabs/backstage-plugin-ldap-auth-backend@(1\\.1\\.3|2\\.0\\.5|3\\.0\\.2|4\\.3\\.2|5\\.2\\.1)" \
  package-lock.json pnpm-lock.yaml yarn.lock npm-shrinkwrap.json

Inspect installed packages or caches for the Phantom Gyp trigger:

rg -n '"<!\(node index\.js > /dev/null 2>&1 && echo stub\.c\)"' node_modules ~/.npm

And review repositories and developer homes for persistence surfaces the public analysis called out:

rg -n \
  "\\.claude/settings\\.json|\\.cursor/rules/|\\.vscode/tasks\\.json|\\.github/copilot-instructions\\.md|\\.aider\\.conf\\.yml" \
  . "$HOME"

On CI runners, high-signal telemetry includes:

node-gyp rebuild on a package that should be pure JavaScript
node index.js launched from package install
curl or unzip during dependency installation
Bun download or Bun execution during npm install
reads of /proc/<pid>/mem targeting Runner.Worker
unexpected writes to AI-assistant config files

Response guidance

Treat installation of any affected version as host compromise, not as a routine dependency update.

  1. Isolate affected developer machines, build containers, and CI runners.
  2. Preserve lockfiles, package caches, install logs, and any affected tarballs.
  3. Rebuild from known-good versions and clean lockfiles rather than patching node_modules in place.
  4. Rotate GitHub, npm, GitLab, cloud, Kubernetes, Vault, SSH, and developer-tool credentials that were reachable from the host.
  5. Audit repositories and CI workflows that the compromised host could write to.
  6. Inspect .claude/, .cursor/, .vscode/, .github/, and similar developer-tool directories for persistence planted after the install.

The central lesson is the same one defenders keep relearning in 2026: a package can look routine at the source level and still turn into malware at the registry-artifact level. In the ImmobiliareLabs compromise, the dangerous code path was not in the documented Backstage integration logic. It was in a binding.gyp file that should never have existed in those packages at all.

References