Skip to content

Security

npm Supply Chain Attacks in 2026: How to Secure Node.js and Laravel Dependencies

After the Shai-Hulud npm worms, here is a practical plan to harden Node.js and Laravel projects: lockfiles, release cooldowns, blocking install scripts, locking down CI and scanning dependencies.

Prakash Tank

Prakash Tank

November 12, 2026 · 11 min read

Summary

Software supply chain attacks have moved from rare headlines to a regular part of running a JavaScript or PHP product. The Shai-Hulud worm campaigns on npm (first in late 2025, then the larger "Mini Shai-Hulud" wave in May 2026 that also reached PyPI) showed how one compromised maintainer account or CI workflow can push malicious versions into thousands of downstream projects within hours. The malware typically runs from an install script, steals cloud keys, GitHub tokens and environment variables, and then uses those credentials to publish even more infected packages.

The good news: most of the damage is preventable with boring, well-understood controls. This guide walks through a practical hardening plan for Node.js and Laravel/PHP teams in 2026: lockfiles and reproducible installs, release cooldowns, blocking install scripts, locking down GitHub Actions, scanning dependencies, and limiting what a compromised build can steal. Every step is something a small team can roll out in a sprint.

How modern package supply chain attacks actually work

It helps to understand the attack chain before choosing defences. Recent npm campaigns followed a similar pattern:

  1. Initial access. The attacker phishes a maintainer's npm credentials, steals a long-lived publish token, or abuses a weak CI workflow (for example a pull_request_target job that runs code from a fork with repository secrets, or a poisoned build cache that a later release job restores).
  2. Malicious publish. A new patch version of a popular package is published. Because it is a patch release, any project using caret (^) or tilde (~) ranges will happily pick it up on the next fresh install.
  3. Execution on install. The payload runs from a preinstall, postinstall or prepare script, sometimes hidden in an optional dependency that points at a git URL or tarball.
  4. Credential theft. It harvests ~/.npmrc, AWS/GCP/Azure keys, GitHub tokens and every environment variable in the CI runner or developer laptop.
  5. Propagation. Stolen npm and GitHub tokens are used to publish infected versions of the victim's own packages, which is why these attacks are called worms.

A key lesson from the 2026 wave is that provenance alone is not enough. Security researchers at JFrog and others documented cases where malicious versions were published from a legitimate trusted-publishing workflow because attacker code was already running inside that workflow. Provenance proves where a package was built, not that the build was clean. You need layered defences.

Layer 1: Lockfiles and reproducible installs

The single most effective habit is making sure production and CI never resolve new versions on their own.

Node.js

  • Commit package-lock.json, pnpm-lock.yaml or yarn.lock, and review lockfile changes in pull requests.
  • In CI and Docker builds, use npm ci (or pnpm install --frozen-lockfile, yarn install --immutable) instead of npm install. These fail if the lockfile and package.json disagree instead of silently upgrading.
  • Update dependencies deliberately through Dependabot or Renovate pull requests, not as a side effect of a deploy.
# Dockerfile: reproducible, script-free dependency install
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts --omit=dev

Laravel / PHP

  • Always commit composer.lock for applications, and deploy with composer install, never composer update, on servers.
  • Use composer install --no-dev --prefer-dist --optimize-autoloader in production builds.
  • Run composer validate --strict in CI to catch a lockfile that is out of sync with composer.json.

Layer 2: Add a release cooldown

Most malicious versions are detected and pulled within hours to a day. If your tooling simply refuses to install anything published in the last day or week, you skip the most dangerous window entirely. This is one of the highest-value, lowest-effort controls available in 2026.

pnpm supports this natively with minimumReleaseAge (in minutes). Recent pnpm versions enable a one-day default; you can raise it for stricter projects:

# pnpm-workspace.yaml
minimumReleaseAge: 10080        # wait 7 days before installing a new version
minimumReleaseAgeExclude:
  - "@your-company/*"           # your own internal packages
trustPolicy: no-downgrade       # block versions published with weaker trust than earlier ones

Yarn, Bun and npm have added similar age-gate settings, but they are generally off by default, so check the documentation for the exact version you run and turn it on. For Dependabot and Renovate, configure a minimum release age or cooldown on update pull requests so your bot does not propose a version that is only minutes old.

// renovate.json
{
  "extends": ["config:recommended"],
  "minimumReleaseAge": "3 days",
  "lockFileMaintenance": { "enabled": true }
}

When you genuinely need an urgent security patch, add a documented exception for that one package instead of turning the cooldown off.

Layer 3: Block install scripts by default

Almost every npm worm so far has executed through lifecycle scripts. Very few packages in a typical web app actually need them (native modules such as esbuild, sharp or bcrypt are the usual exceptions).

  • npm: set ignore-scripts=true in the project .npmrc and run the few required build steps explicitly (for example npm rebuild sharp).
  • pnpm: recent versions do not run dependency build scripts unless you allow them. Keep that allowlist short and review every addition.
  • Composer: since Composer 2.2, plugins must be explicitly allowed through config.allow-plugins. Do not set it to true for everything.
// composer.json (excerpt)
{
  "config": {
    "allow-plugins": {
      "pestphp/pest-plugin": true,
      "php-http/discovery": false
    },
    "sort-packages": true
  }
}

Also block "exotic" dependency sources. A transitive dependency that resolves to a GitHub commit or a raw tarball URL is a red flag in a production app; pnpm can block these for subdependencies, and in code review you should question any git+ or https:// version spec in package.json or a custom VCS repository in composer.json.

Layer 4: Lock down your CI/CD pipeline

Your CI runner is the most valuable target in your stack: it usually holds deploy keys, cloud credentials and publish rights. Treat GitHub Actions (or GitLab CI, Bitbucket Pipelines) as production infrastructure.

GitHub Actions checklist

  • Set a default of least privilege at the top of every workflow: permissions: contents: read, and grant more only to the job that needs it.
  • Pin third-party actions to a full commit SHA, not a mutable tag like @v4.
  • Avoid pull_request_target unless you fully understand it; never check out and run fork code in that context.
  • Keep release and publish jobs separate from test jobs, and do not restore shared dependency caches in a job that has publish or deploy credentials.
  • Use OIDC to get short-lived cloud credentials (AWS IAM roles, Google Workload Identity Federation) instead of storing long-lived access keys as secrets.
  • Protect release environments with required reviewers.
# .github/workflows/ci.yml
name: ci
on: [pull_request]
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<full-commit-sha> # v4, pinned by SHA
      - uses: actions/setup-node@<full-commit-sha> # v4, pinned by SHA
        with:
          node-version: 22
      - run: npm ci --ignore-scripts
      - run: npm audit signatures
      - run: npm test

(Replace the placeholders with the full commit SHAs you verify for the action versions you use; tools like Dependabot can keep pinned SHAs up to date.)

If you publish packages yourself

If your team maintains npm packages or an internal SDK, switch to npm trusted publishing with OIDC instead of long-lived tokens, require 2FA for publishing on the account and organisation, and publish only from a dedicated, minimal release workflow that does not run untrusted code or restore caches built by pull requests.

Layer 5: Scan dependencies continuously

Scanning will not stop a zero-day malicious package by itself, but it catches known-bad and vulnerable versions quickly and gives you an inventory when the next incident hits.

  • npm audit / pnpm audit and composer audit (Composer 2.4+) as a CI step, failing the build on high-severity advisories.
  • npm audit signatures to verify registry signatures and provenance attestations where available.
  • GitHub Dependabot alerts or an equivalent SCA tool for vulnerable versions.
  • A behaviour-based malware scanner (Socket, Snyk, and similar products) that inspects new package versions for suspicious install scripts, network calls and obfuscation.
  • Generate an SBOM (for example with CycloneDX tooling) for each release so you can answer "are we affected?" in minutes rather than days.
# CI step for a Laravel + Vite project
composer validate --strict
composer install --no-interaction --prefer-dist --no-progress
composer audit --no-dev
npm ci --ignore-scripts
npm audit --audit-level=high

Layer 6: Reduce the blast radius

Assume one day something will slip through. The question is what it can reach.

  • Developer machines: run dependency installs inside dev containers or Docker (Laravel Sail works well for this), so a malicious script sees a container filesystem rather than your home directory, SSH keys and browser profiles.
  • Secrets: keep production secrets out of developer laptops and out of build stages. Load them at runtime from AWS Secrets Manager, Google Secret Manager or your platform's secret store.
  • Tokens: prefer short-lived, narrowly scoped tokens. A GitHub fine-grained token for one repository is far less dangerous than a classic token with full repo scope.
  • Network egress: build runners that can only reach your registry and package mirrors make exfiltration much harder. Self-hosted runners with egress rules or tools that monitor runner network traffic can help.
  • Private registry or proxy: larger teams often route installs through an internal proxy (such as Verdaccio, Artifactory or a cloud artifact registry) where they can quarantine new versions centrally.

What to do if you installed a compromised package

  1. Identify affected versions from the advisory and search all lockfiles across your repositories for them.
  2. Assume every secret present on the affected machine or runner is stolen. Rotate npm tokens, GitHub tokens, cloud keys, database passwords and API keys, starting with the most privileged.
  3. Review cloud audit logs (CloudTrail, Google Cloud Audit Logs) and GitHub audit logs for unusual activity, new repositories, new workflows or unexpected package publishes.
  4. Pin to a known-good version, clear CI caches, and rebuild images from scratch.
  5. Write a short post-incident note and turn what you learned into the controls above.

A realistic two-week rollout plan

You do not need a dedicated security team to get most of the benefit:

  • Days 1 to 3: enforce lockfiles, switch CI and Docker to npm ci / composer install, add composer audit and npm audit to CI.
  • Days 4 to 6: enable a release cooldown in your package manager and update bot; disable install scripts by default and allowlist the few you need.
  • Days 7 to 10: set workflow permissions to read-only by default, pin actions to SHAs, move cloud access to OIDC, and remove long-lived keys from CI secrets.
  • Days 11 to 14: move local installs into containers, add an SBOM step, and write a one-page incident playbook with a secret-rotation checklist.

For a broader view of application-level risks, pair this with our guide to the OWASP API Security Top 10 for Laravel and Node.js and our Laravel web application security checklist.

FAQ

Is npm more dangerous than Composer?

Not inherently, but the npm ecosystem has far more small transitive dependencies and widely used install scripts, which gives attackers more entry points. Composer and Packagist are targeted too, so the same principles (lockfiles, audits, limiting plugins, reviewing new dependencies) apply to Laravel projects.

Does a release cooldown leave me exposed to known vulnerabilities for longer?

Slightly, which is why you keep an exception path for urgent security patches. In practice, a one- to seven-day delay on routine updates removes far more risk from malicious releases than it adds from delayed patches.

Is npm provenance or trusted publishing enough to trust a package?

No. It is a useful signal and makes stolen tokens less valuable, but 2026 incidents showed malicious versions can still be published from a compromised trusted workflow. Combine provenance with cooldowns, script blocking and scanning.

Should we stop using open-source packages?

No. Open source is essential and generally well maintained. The goal is to install it deliberately: fewer unnecessary dependencies, pinned versions, reviewed updates and limited permissions for the tools that install them.

How often should we rotate CI secrets?

Ideally you replace long-lived CI secrets with short-lived OIDC credentials so there is little to rotate. For secrets that must remain, rotate on a schedule and immediately after any suspected compromise.

Can DayDecoder audit our existing Laravel or Node.js pipeline?

Yes. We review dependency hygiene, CI/CD configuration, secret handling and cloud permissions, then implement the fixes with your team.

Prakash Tank

Written by

Prakash Tank

Founder & Full-Stack Architect, DayDecoder

Prakash started as a certified Laravel developer and grew into a full-stack architect who owns a project end to end: requirements, architecture, development, deployment, and support. He designs event-driven systems with...

More about Prakash

Keep reading

Related articles

Let's talk

Turn this into a plan for your project.

Share what you are building. We will reply within one business day with honest advice and next steps.

  1. 1

    We reply within 24 hours

    A real person reads your message and replies on the next business day at the latest.

  2. 2

    Free 30-minute call

    We talk through your goals, users, and budget, and answer every question.

  3. 3

    Proposal & fixed quote

    You get a written scope, timeline, and price. No obligation.

contact@daydecoder.com Ahmedabad, India

Get a free consultation

Takes under a minute. No spam, no obligation.

What do you need help with?
46 + 8 =

Your details stay private. NDA on request.