Most shop operators think about security in terms of what customers see: login, checkout, payment data. The Shai-Hulud waves exposed a more uncomfortable truth. The largest attack surface is not the shop in the browser, it is the build. Malicious code spreads through npm in a self-replicating fashion, now hits Packagist and PyPI as well, and the payload runs in the preinstall phase — before any test can catch it. This article shows how a storefront build chain is compromised in practice and which measures actually harden your development and deployment processes.

Why the build is more dangerous than the shop in the browser

An attacker who wants to take over your storefront has two routes. The first runs through the frontend: SQL injection, XSS, session hijacking, brute force against the login. Most shops have something built against that route — a WAF, rate limits, maintained updates. The second route runs through the build. And there, usually nothing stands in the way.

The difference is severe. A frontend attack gives the attacker what the application grants. A build attack gives them whatever your CI runner is allowed to do: deploy keys, database credentials, cloud credentials, payment API keys, registry tokens — in many pipelines, effectively everything. Running composer update on a runner is therefore more privileged than any customer interaction in the shop.

The basis for this is the sheer volume of foreign code. A storefront project pulls several hundred to several thousand transitive dependencies through npm and Composer. You consciously selected perhaps twenty; the rest is code written by people you do not know, executed with your pipeline's privileges. For July 2024 to June 2025, the German Federal Office for Information Security (BSI) records an average of 119 (BSI) new vulnerabilities per day, roughly 24% (BSI) more than the year before, alongside a 38% (BSI) rise in exploitation.

That the supply chain is a distinct problem is shown by a second example from the same report: in two IoT botnets, the malware reached the devices during the production process. Around 40,000 (BSI) entered retail pre-infected, with no effective countermeasure available to users. That is the logic of a supply chain attack: the damage occurs before the affected party can decide anything.

The shift in perspective

The relevant question is not Is my shop secure? but What could any npm or Composer package do inside my pipeline if it turns malicious tomorrow? Anyone who cannot answer that does not have a future supply chain problem — they have an open flank today. Our overview of IT security in e-commerce puts the fundamentals in context.

Shai-Hulud: a worm that passes itself along

On 23 September 2025, the US cybersecurity agency CISA published an alert about a widespread compromise of the npm ecosystem. A self-replicating worm publicly known as Shai-Hulud had compromised roughly 500 (CISA) packages at that point. After gaining initial access, the malware scanned the environment for sensitive credentials, specifically targeting GitHub Personal Access Tokens and API keys for cloud services including AWS, Google Cloud Platform and Microsoft Azure (CISA).

The decisive mechanism is self-replication. Palo Alto Networks Unit 42 describes it precisely: the malware harvests npm tokens from compromised developers, then identifies other packages maintained by that developer, injects malicious code into them, and publishes the new, compromised versions to the registry (Unit 42). After that, the attack no longer needs an attacker. It simply runs.

The second wave in November 2025 was considerably larger. Trojanised packages were uploaded between 21 and 23 November 2025 (The Hacker News). Unit 42 counts more than 25,000 (Unit 42) malicious repositories across roughly 350 (Unit 42) unique user accounts. At the time of reporting the figure had reached more than 27,000 (The Hacker News) repositories, with around 1,000 (The Hacker News) new repositories added every 30 minutes. Maintainer accounts from projects such as Zapier, ENS Domains, PostHog and Postman were affected (The Hacker News). Microsoft dates the campaign start to 24 November 2025 (Microsoft Security).

It also added a destructive fallback: if theft fails, the malware overwrites and deletes every writable file owned by the user under their home folder (Unit 42). A data leak turns into a total loss of the working environment.

Registry tokens

npm access tokens from .npmrc files — the fuel of self-replication: every captured token infects further packages (Unit 42).

GitHub access

Personal access tokens and Actions secrets. With those, the attacker owns not just the build but the source code.

Cloud credentials

Environment variables holding AWS, GCP and Azure keys (CISA), and lately AWS Secrets Manager and the Instance Metadata Service (Microsoft Security).

Secret stores

Vault, Kubernetes tokens and password managers. The payload searches more than a dozen token paths in parallel (Microsoft Security).

SSH keys

Private keys from the home directory (Unit 42) — they open servers that have nothing to do with the build chain.

Wallets and browser data

Cryptocurrency wallets and browser profiles — the campaign is regarded as financially motivated.

May 2026 brought another wave, tracked as Mini Shai-Hulud, via the taken-over npm organisation @antv. GitHub removed 640 (Microsoft Security) malicious packages and invalidated 61,274 (Microsoft Security) granular npm access tokens with write permissions. The payload was a roughly 499 KB (Microsoft Security) obfuscated JavaScript file that ran silently during npm install; more than 2,200 (Microsoft Security) attacker-created repositories were observable.

The pattern behind the waves

Not one of these attacks needed a vulnerability in your code. It only needed you to pull an update. The attack chain starts at someone else's maintainer account and ends in your pipeline — and in between lies exactly one decision you control: when and how you adopt new versions.

preinstall: the payload runs before any test

The technically most important point for shop operators is the execution phase. The original Shai-Hulud variant ran via postinstall scripts, meaning after installation. The November escalation shifted to preinstall, dramatically widening the impact across developer machines and CI/CD pipelines without requiring human interaction (Unit 42). Microsoft describes the same core: malicious code executes during the preinstall phase of infected npm packages, allowing execution before tests or security checks (Microsoft Security).

That is not a detail but a break with a widespread assumption. Many teams believe their pipeline is a filter: install, test, scan, deploy — if something is wrong, the test catches it. With a preinstall payload, that ordering is worthless: the code has exfiltrated the tokens before the first test suite starts. A scanner that fires afterwards documents an incident, it does not prevent one.

package.json (of an infected package)
{
  "name": "harmless-looking-utility",
  "version": "4.2.1",
  "scripts": {
    "preinstall": "node setup_bun.js"
  }
}

For this campaign, Microsoft documents a preinstall script named setup_bun.js, also referenced as set_bun.js (Microsoft Security). The name is arbitrary and looks like build tooling. What matters is not the name but the fact that a lifecycle hook exists and your package manager executes it without asking.

PhaseWhen it runsWhat still catches it beforehand
postinstallAfter installationResolution and download, nothing else
preinstallBefore installationNothing. No test, no linter, no scanner
Composer autoload.filesOn every PHP requestScript blocks do not apply here
Test suiteAfter the installToo late for both script phases

The consequence is uncomfortable, but it is the starting point of any workable defence: the only control point that sits ahead of a preinstall payload is the decision whether this package, in this version, gets installed at all — and whether scripts are allowed to run while it does. Everything else arrives too late.

Composer is hit just as hard — and more subtly

Anyone coming from the PHP world, running Shopware, WooCommerce or a custom storefront, might file the npm problem under JavaScript. That held until 2026. On 22 and 23 May 2026, in rapid succession with many versions appearing only seconds apart, an attacker republished hundreds of malicious versions under historical release tags for four localisation libraries in the laravel-lang namespace on Packagist (The Hacker News). More than 700 (The Hacker News) affected versions were identified in total.

The method deserves particular attention because it defeats a widely used safeguard. The attacker did not publish a new version. They rewrote every existing git tag in each repository to point at a new malicious commit (The Hacker News). Version pinning protects against new versions. It does not protect when the old, reviewed version suddenly contains different code under the same number.

The execution path was different too. Instead of a lifecycle script, the attacker added src/helpers.php to the autoload.files map of each package (The Hacker News). Since every Laravel application calls require __DIR__.'/vendor/autoload.php' on startup, the payload ran on every PHP request. The stealer comprised roughly 5,900 (The Hacker News) lines of PHP, harvested cloud credentials, CI tokens, browser data and SSH keys, encrypted everything with AES-256 and then deleted itself.

Why --no-scripts is not enough here

The laravel-lang attack ran through the Composer autoloader, not through a lifecycle script. Script blocks such as --no-scripts do not apply at that point. This is not an argument against script control — it is an argument that a single measure does not hold. Only the combination of a cooldown, reviewing the lockfile diff and constrained privileges covers both execution paths.

Another May 2026 campaign worked cross-ecosystem: across eight Packagist packages, the malicious code sat not in composer.json but in package.json (The Hacker News) — the targets being projects that ship JavaScript build tooling alongside PHP, in other words the typical modern storefront. A postinstall script downloaded a Linux binary from a GitHub Releases URL to /tmp/.sshd and ran it. The same payload was found across 777 (The Hacker News) files on GitHub. Anyone scanning only Composer metadata sees none of it.

Packagist names the incidents openly: intercom/intercom-php on 30 April 2026 and laravel-lang on 22 May 2026, both via stolen credentials (Packagist). The countermeasures show which assumptions no longer hold: stable versions can no longer be silently rewritten through git re-tagging, tag changes are rejected and reported. Composer 2.10 introduces a dependency policy framework covering flagged versions, vulnerabilities and abandoned packages; mandatory MFA and a FIDO2-backed release flow are planned (Packagist).

How a storefront build chain is compromised in practice

Abstract attack classes rarely win a budget meeting. The following chain translates the documented incidents directly into the daily reality of a storefront. It begins far outside your sphere of influence and ends in your production environment.

  1. A maintainer of a package three levels below your direct dependency loses their registry token — through phishing, a leak or a previous infection (Unit 42).
  2. The worm publishes new versions of every package that maintainer looks after under that account. The malicious code sits in a preinstall hook (Microsoft Security).
  3. Your Renovate bot, a developer or a nightly build runs npm install or composer update. The new version satisfies your version constraint and gets resolved.
  4. The preinstall hook runs on the CI runner. Before the test. Before the scan. Before the build.
  5. The payload searches environment variables, metadata endpoints, Vault paths and .npmrc for credentials (Microsoft Security).
  6. The loot is exfiltrated to a public, attacker-controlled repository (Microsoft Security).
  7. Using your registry token, the worm infects your own packages next. You turn from victim into distributor.

What happens next depends solely on the privileges your runner held: in the unfavourable case, production deploy keys, database credentials, the payment API key and the object storage holding customer data. The worm does not have to guess them — they sit in the environment of the process it just started.

The payload does not run because someone made a mistake. It runs because someone pulled an update — and because nothing stood between the registry and the runner.

XICTRON development team

Whether you run a Shopware storefront, operate your own frontend stack or build on WordPress: as soon as a package manager is involved, the chain is identical. Complementary hardening at application level is covered in our article on WordPress security.

Commit lockfiles and actually review them

The first measure costs nothing and is still skipped regularly: composer.lock and package-lock.json belong in the repository. Without a lockfile, every build resolves dependencies afresh, which makes every build a different program. With a lockfile the blueprint is fixed — and, just as importantly, every change to it becomes a visible diff in the pull request.

From that follows the separation of commands. composer install and npm ci install exactly what the lockfile says. composer update and npm install are allowed to raise versions and rewrite the lockfile. The first set belongs in the pipeline, the second on a developer machine and in a deliberate pull request.

Terminal
$ composer install --no-scripts --no-plugins
Installing dependencies from lock file (including require-dev)
$ npm ci --ignore-scripts
added 1284 packages in 9s
$ git diff --stat main -- composer.lock package-lock.json
composer.lock | 41 ++++++++------- package-lock.json | 96 ++++++++++++++++++--------

The third command is the actual control point. A lockfile diff that suddenly spans ninety lines for a single package patch update and touches packages nobody requested is a signal. CISA recommends exactly this look: check package-lock.json or yarn.lock files to identify affected packages, including those nested in dependency trees, and search for cached versions of affected dependencies in artifact repositories and dependency management tools (CISA).

  • composer.lock and package-lock.json are versioned, not in .gitignore.
  • CI uses install commands exclusively, not update.
  • Lockfile changes require review and are not auto-merged.
  • Caches in artifact repositories are checked during an incident as well (CISA).
  • Automated update bots open pull requests; they do not merge on their own.

Script control: --ignore-scripts as the default

If the payload sits in preinstall, the most obvious countermeasure is not to let scripts run in the first place. That is exactly what the agencies recommend. In the response to the npm compromise, the wording is unambiguous: disable npm postinstall scripts in CI where possible (CISA). Microsoft names npm install --ignore-scripts as the first mitigation in its analysis of the May 2026 wave (Microsoft Security).

Terminal
$ npm config set ignore-scripts true
$ cat .npmrc
ignore-scripts=true min-release-age=7
$ composer install --no-scripts --no-plugins
Generating optimized autoload files

The honest part: the recommendation is not free. Some packages genuinely need their build step, natively compiled modules in particular. The pragmatic path is therefore not a blanket ban but a reversal of the burden of proof: scripts are off by default, and where a package needs them, it gets named and documented. A silent permission for a thousand packages becomes a deliberate permission for three.

Script blocks do not cover everything

--no-scripts prevents Composer lifecycle scripts but not code loaded via autoload.files — the path the laravel-lang attack used (The Hacker News). On the npm side, --ignore-scripts works against preinstall and postinstall, but not against malicious code that executes when your application code first uses the module. Script control is an effective layer, not a complete boundary.

Version pinning and the cooldown window

Pinning means fixing exact versions instead of allowing ranges such as ^4.2.0. For this specific incident, CISA puts it very directly: pin npm package dependency versions to known safe releases produced prior to 16 September 2025 (CISA). Pinning is effective, but it has the limit the laravel-lang case exposed — when tags are rewritten, the same version number points at different code (The Hacker News). That is why the version immutability Packagist introduced matters so much (Packagist).

The second measure, and in our view the most effective and most underestimated one, is the cooldown window: new versions simply are not adopted for a while. The logic is straightforward — malicious versions are typically detected and pulled within hours, while the attack depends on automated pipelines consuming them immediately. Wait a week and you are simply not part of most of these campaigns.

This is now a setting, not a concept. The npm CLI introduced the min-release-age option in version 11.10.0 (npm CLI), released on 10 February 2026. It takes a number of days and filters out every version younger than that (npm CLI). Packagist plans a corresponding minimum release age policy that refuses installation of versions published less than a defined period ago (Packagist).

.npmrc
# Scripts off by default
ignore-scripts=true

# Do not install any version younger than 7 days
min-release-age=7

# Internal registry as the only source
registry=https://npm.internal.example.com/
MeasureWorks against preinstallWorks against tag rewriteEffort
Commit lockfilePartiallyYes, via hash checkVery low
--ignore-scriptsYesNoLow to medium
Version pinningPartiallyNoLow
Cooldown windowYesYesVery low
Registry proxyYes, on approvalYesMedium

The objection is obvious: what about urgent security updates? A cooldown needs a deliberate exception — a critical patch is pulled forward manually and with a stated reason. That costs a few minutes, a few times a year, and is a good trade against a window in which pipelines blindly collect every freshly published version.

Registry proxy and SBOM: know what comes in

An internal registry proxy is the place where trust turns into a decision. Instead of pulling directly from the public registry, every runner fetches packages from an internal mirror. The proxy can hold versions back, enforce approvals and block flagged packages ecosystem-wide — and it answers the question asked first in every incident: what have we pulled over the past few weeks?

The second half of that answer is the SBOM, the software bill of materials. With Technical Guideline TR-03183-2 (BSI), the BSI has defined formal and technical requirements for software bills of materials. An SBOM documents which commercial and free software components are contained in software products, makes third-party dependencies transparent, and helps manufacturers, security researchers and professional users monitor vulnerabilities (BSI). The BSI also classifies SBOMs as a central requirement of the European Cyber Resilience Act (BSI) — what that means in regulatory terms is covered in our article on the obligations arising from the Cyber Resilience Act.

The practical value shows in an emergency. For detecting Shai-Hulud 2.0 packages, Microsoft describes exactly this procedure: an SBOM is created in the background and a lookup determines whether any package in the filesystem or repository is a known malicious package (Microsoft Security). Without a bill of materials, Are we affected? is a multi-day search. With one, it is a query.

  • Generate an SBOM per build, not once a year. A bill of materials that does not reflect the latest release answers the wrong question.
  • Choose formats per TR-03183-2 so the bill stays machine-readable and usable for CRA evidence (BSI).
  • Retain build artifacts and map them to the SBOM, so an incident can pinpoint exactly which deployment is affected.
  • Configure the internal registry as the only source in .npmrc and composer.json, otherwise the proxy is merely a suggestion.
  • Ingest advisories automatically, so a package flagged as malicious stops the next build instead of generating an email.

Least-privilege tokens and short-lived CI secrets

Once you accept that malicious code can run in your pipeline, the question is no longer How do I keep it out? but What does it find? The payload does not steal privileges that are not there: every token not sitting in the environment cannot leak.

The registries have drawn that conclusion themselves. On 9 December 2025, npm permanently revoked all classic tokens — they can no longer authenticate, be recreated, or be recovered (GitHub Changelog). Since then, npm login issues session tokens valid for two hours, granular write tokens are limited to a maximum of 90 days (GitHub Changelog), and OIDC-based trusted publishing is recommended for publishing, which removes token management entirely (GitHub Changelog).

The same logic applies to your pipeline. Microsoft explicitly recommends removing unnecessary roles and permissions granted to identities assigned to CI/CD pipelines (Microsoft Security). In practice that means an uncomfortable inventory: the runner that builds storefront assets needs no write access to the production database, no payment API key and no publish rights. Usually it has all three, because at some point a job needed them.

  • Separate build and deploy. The job that executes foreign code does not deploy. The job that deploys does not execute foreign code.
  • No publish rights in the standard build. A token without publish rights cannot carry the worm onward — precisely the mechanism that made Shai-Hulud large (Unit 42).
  • Short-lived credentials instead of stored keys. OIDC-based flows issue credentials for the lifetime of a job; an exfiltrated token is worthless afterwards.
  • Protect metadata endpoints. The payload specifically queries AWS IMDS and Secrets Manager (Microsoft Security).
  • Do not set secrets as broad environment variables; inject them per step instead. The environment is the first thing searched (CISA).
  • Phishing-resistant MFA on all developer accounts, especially at the registries (CISA).

Thinking that through consistently lands on the same principle we describe for network and runtime in Zero Trust for online shops: no implicit trust, not even for your own build runner. How that maps onto a specific cloud infrastructure depends on the platform; the principle stays the same.

Incident runbook: what counts in the first hours

When an advisory like the one for Shai-Hulud appears, time is the decisive quantity. The following steps summarise what CISA, Unit 42 and Microsoft recommend consistently — in order of effect.

  1. Find and remove affected packages. Scan across all endpoints for the reported packages and versions, removing any that are found (CISA).
  2. Rotate all credentials, immediately. npm tokens, GitHub PATs, SSH keys and cloud access keys of the affected developers and runners (Unit 42). Rotate what the process could read, not what you believe was affected.
  3. Discard build artifacts. Every artifact from a build that could have pulled a compromised version counts as unusable. Caches in artifact repositories and dependency management tools belong in scope (CISA).
  4. Isolate affected CI agents instead of letting them keep running (Microsoft Security).
  5. Review repositories. Look for unknown repositories, unexpected branches and suspicious commits — the worm creates repositories of its own (Unit 42).
  6. Evaluate secret store access logs, such as Key Vault access, to bound the exposure window in time (Microsoft Security).
  7. Freeze updates until the picture is clear (CISA).
  8. Verify deployments. Rebuild from reviewed sources and reconcile against the expected lockfile before shipping again.
The most expensive mistake during an incident

Rotating too narrowly. The laravel-lang stealer harvested cloud keys, database and queue credentials, third-party API tokens, OAuth client secrets, SSH and signing keys as well as Vault and Kubernetes tokens (The Hacker News). The payload ran with the privileges of the PHP process. What needs rotating is therefore everything that process could read — including things that had nothing to do with the package.

The last point is readily skipped because it sounds like a formality. It is not: if a build was compromised, the shipped artifact is the actual damage — it runs in production and talks to your customers. A clean rebuild from reviewed sources, reconciled against the expected lockfile, is the point at which an incident ends.

The supply chain belongs in your remit

There is no single measure that covers everything. What there is, is a set of cheap decisions with high leverage: commit lockfiles and read their diffs, turn scripts off by default, set a cooldown window, keep tokens short-lived and without publish rights, know what was built. Taken together, they turn an automated attack path into one that requires several deliberate wrong decisions.

The effort is unevenly distributed: a cooldown and ignore-scripts=true are two lines in a config file, while a registry proxy with SBOM generation and a privilege inventory is a project. In our experience it pays to proceed in that order: first the lines that take effect today, then the structure behind them (project experience).

This is not only about defence. A build chain in which every dependency is adopted deliberately is also the one in which you ship faster — because nobody has to guess what an update brought along. The same discipline carries through migrating Shopware plugins to 6.7, tuning shop search with OpenSearch, and legally sensitive features such as labelling strikethrough prices correctly, where an unnoticed frontend change turns into legal exposure.

XICTRON hardens build pipelines and dependency processes in client projects: script control and cooldown windows as standard, a privilege inventory of the CI, an SBOM per build, and a runbook that holds up during an incident. If you are not sure what your runner is allowed to do today, that is a good starting point for a conversation about your development processes. The same principle at a different entry point is shown in our article on securing AI features in the shop.

Sources and studies

This article draws on: CISA, Alert: Widespread Supply Chain Compromise Impacting npm Ecosystem, 23 September 2025 (cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem), Microsoft Security Blog, Shai-Hulud 2.0: Guidance for detecting, investigating, and defending against the supply chain attack, 9 December 2025, and Mini Shai-Hulud: Compromised @antv npm packages enable CI/CD credential theft, 20 May 2026 (microsoft.com/en-us/security/blog), Palo Alto Networks Unit 42, Shai-Hulud Worm Compromises npm Ecosystem in Supply Chain Attack (unit42.paloaltonetworks.com/npm-supply-chain-attack), BSI, The State of IT Security in Germany 2025 (reporting period 1 July 2024 to 30 June 2025) and BSI, Technical Guideline TR-03183-2, Cyber Resilience Requirements: Software Bill of Materials (bsi.bund.de), The Hacker News, reporting on the npm, Packagist and laravel-lang incidents 2025/2026 (thehackernews.com), Packagist, An update on Composer & Packagist supply chain security (blog.packagist.com), and GitHub Changelog and npm CLI, documentation on token revocation, granular tokens and min-release-age (github.blog, github.com/npm/cli). Our own project experience is incorporated as well. The figures cited may vary depending on the time and methodology of collection.

Frequently asked questions

As a sole measure, typically not. A lockfile freezes the blueprint and makes changes visible in the pull request — that is the foundation. It does not protect at the moment you deliberately update the lockfile, and it does not protect against scripts that run while installing the pinned version. In our experience, only the combination of lockfile, script control and a cooldown window holds, because they cover different points in the chain.

Typically yes, with exceptions. Most packages in a storefront stack do not need their lifecycle hook; natively compiled modules are the main complication. The usual approach is therefore to disable scripts by default and approve individual packages by name. CISA recommends disabling npm postinstall scripts in CI where possible (CISA); Microsoft names npm install --ignore-scripts as the first mitigation (Microsoft Security). Both frame it as a default with justified exceptions rather than all or nothing.

It depends on your release cadence. The npm CLI has supported the min-release-age option, measured in days, since version 11.10.0 (npm CLI). In practice many teams settle between one and seven days: one day already catches a large share of the fast-moving campaigns, because malicious versions are typically detected and pulled within hours; seven days add further safety but also delay regular updates. In both cases what matters is a defined way to pull a critical security patch forward with a stated reason.

Yes. The laravel-lang incident in May 2026 ran entirely through Composer: more than 700 versions of four packages were compromised via rewritten git tags, the payload sat in the autoloader and ran on every PHP request (The Hacker News). On top of that, modern storefronts are rarely genuinely npm-free — in another campaign, attackers placed the malicious code across eight Packagist packages specifically in package.json, because many PHP projects ship JavaScript build tooling alongside (The Hacker News).

Reliably only with records created before the incident. CISA recommends checking package-lock.json or yarn.lock for affected packages, including deeply nested dependencies, and additionally searching for cached versions in artifact repositories (CISA). For detection, Microsoft describes generating an SBOM and then reconciling it against known malicious packages (Microsoft Security). Without lockfile history and without a bill of materials, what usually remains is a laborious manual reconstruction.

In our experience, more than initially assumed. The yardstick is not the compromised package but the process it ran in: everything that process could read. Unit 42 names npm tokens, GitHub PATs, SSH keys and cloud access keys (Unit 42); with the laravel-lang stealer, database, queue and cache credentials, third-party tokens, OAuth client secrets, signing keys as well as Vault and Kubernetes tokens were added (The Hacker News). In parallel, build artifacts from the period in question should be discarded and affected CI agents isolated (Microsoft Security).