how to detect supply-chain tampering in npm packages before CI deploys using free tools and automated reproducible builds

how to detect supply-chain tampering in npm packages before CI deploys using free tools and automated reproducible builds

I want to share a practical, hands‑on approach I use to detect supply‑chain tampering in npm packages before anything reaches CI deploys. Over the years I’ve combined small, free tools and reproducible‑build practices to make a fast, local gate that catches the most common and stealthiest tricks attackers use — malicious postinstall scripts, rogue tarballs, or silently altered published code. Below I walk through the mindset, the checks I run locally, and a few short scripts and commands you can adopt immediately.

Threat model and goals

First, be explicit about what I’m trying to stop: unauthorized or malicious changes to code that your build will pull from the npm registry (or alternative registries), and arbitrary code execution from install hooks. I’m not covering zero‑day vulnerabilities in Node itself; instead the focus is supply‑chain tampering: changed package contents, replaced tarballs, or packages published with backdoors.

My objectives when I examine an npm dependency tree before CI are:

  • Make sure package tarballs match what package-lock.json (or pnpm/yarn lock) claims.
  • Prevent install scripts from running until I’ve verified content.
  • Detect unsigned or unexpected provenance, and flag packages that don’t provide reproducible artifacts.
  • Automate these checks so they run locally or as an early CI job with free tooling.
  • Baseline: lockfiles, pinned runtime and hermetic installs

    A lot of supply‑chain safety starts with basics you should already be doing. Commit a lockfile (package-lock.json, pnpm-lock.yaml, or yarn.lock) and pin your Node and npm versions (use .nvmrc or engines in package.json). That gives a deterministic list of tarball URLs and sha hashes which you can verify. Also prefer using a hermetic install for verification: run installs inside a throwaway Docker container or a clean VM so environment differences don’t mask tampering.

    I also recommend running installs with scripts disabled while you verify artifacts. Use: npm ci --ignore-scripts. This prevents postinstall hooks from running until you decide it’s safe.

    Step 1 — Verify integrity fields from the lockfile

    package-lock.json normally contains an "integrity" field with a base64 SHA512 (or sha1 legacy) for each resolved package. You can programmatically download the tarball URL in the lockfile and recompute the hash locally to ensure the remote file matches the lockfile expectations.

    Workflow I use:

  • Extract the "resolved" tarball URL and "integrity" value from package-lock.json for each dependency.
  • Download the tarball with curl or wget into a tmp directory.
  • Unpack or compute the SHA512 digest of the tarball and compare to the lockfile's base64 digest.
  • Example command flow (conceptual): download the URL, compute sha512, base64 encode and compare to the integrity value. If you want a single‑shot script, a tiny Node or shell script can iterate package-lock.json entries and fail if any mismatch occurs. Any mismatch is a red flag — either the lockfile was tampered with, or the registry is serving different content than when the lockfile was generated.

    Step 2 — Inspect tarball contents before running scripts

    After you download matching tarballs, inspect them before allowing installs that run scripts. You can do: tar -tzf package.tgz to list files and search for suspicious items like author scripts, cryptic binaries, or .node compiled modules you don't expect. Also search package.json within the tarball for "scripts" fields and look for postinstall, preinstall or prepare hooks.

    If you prefer automation, write a short check that fails CI if any tarball contains package.json with a "scripts" entry that includes postinstall or prepare. That way you keep the default install path safe by consciously authorizing packages that need scripts.

    Step 3 — Use npm ci --ignore-scripts then run reproducible build checks

    Install dependencies without executing scripts: npm ci --ignore-scripts. Once you have node_modules populated, you can run your build steps in a reproducible environment and compare outputs against a known good artifact hash. The reproducible‑build idea is simple: perform the same build twice (locally and in a clean environment) and ensure produced artifacts match bit‑for‑bit. If a dependency modified itself during install via a script, the builds will diverge or you’ll find unexpected files in node_modules.

    I do this by building in a pinned Docker image (matching my .nvmrc), producing an artifact (for example a bundle or package .tgz), and computing a sha256sum. Repeat the build in CI or another clean environment and compare checksums. Differences indicate something non‑deterministic — which may be a benign environment dependency or a sign of tampering.

    Step 4 — Leverage free provenance and signature tools

    More projects are starting to publish signatures and SLSA provenance. Sigstore (cosign, rekor) is free and increasingly supported. If a package publishes a signed artifact or in-toto metadata, verify it with cosign or in-toto verifier. For many npm packages this is not yet universal, but packages that offer signatures are higher trust — verify and prefer them.

    Commands you can adopt:

  • Use cosign to verify an artifact signature if the publisher provides it.
  • Check public transparency logs (Rekor) to confirm the signature was recorded.
  • If a package lacks signatures, fall back to the lockfile/tarball checks above.

    Step 5 — Automated checks to run locally or as a pre‑CI gate

    I automate the following checks in a lightweight script that runs on developer machines and as an early CI job (fast, runs before secrets or deploy steps):

  • Ensure .nvmrc matches allowed Node versions.
  • Run npm ci --ignore-scripts to avoid running arbitrary code.
  • Extract resolved URLs + integrity from lockfile, download each tarball and verify the SHA512 integrity matches the lockfile.
  • List files in each tarball and fail if risky scripts are present (postinstall, prepare).
  • Build reproducible artifact in a pinned Docker image and produce a hash to compare with baseline.
  • Run npm audit --audit-level=moderate to catch known vulns (not tampering, but useful).
  • All of these steps can be implemented with a few dozen lines of Bash or Node. I keep mine open and intentionally minimal — it should be fast so developers run it frequently.

    Practical tips and gotchas

    Ignore-scripts isn’t a panacea: Some legitimate packages require build steps. For those, I make an allowlist and require additional review and provenance checks (signatures, source matching) before permitting scripts to run.

    Lockfile hygiene: Recreate lockfiles in a controlled environment if you suspect they were generated on a compromised machine. Regenerating a lockfile locally without access to the original tarballs may hide tampering; prefer to compare tarball hashes to a known‑good baseline.

    Registry mirrors and proxies: If you use a private registry proxy (Verdaccio, Artifactory, npm Enterprise), verify the proxy stores integrity and signatures and periodically refresh from the upstream registry.

    Automate but review failures: Automated checks will flag many benign differences in non‑deterministic builds. When a check fails, treat it as an incident to triage — compare tarball contents, check package changelog, and contact maintainers when necessary.

    Quick example script idea (concept)

    A minimal approach: parse package-lock.json entries, download resolved tarball, compute sha512, compare to integrity. If any mismatch, abort. Then run npm ci --ignore-scripts. If a package needs scripts, require an explicit signed provenance check before re‑allowing scripts. This can be a single preinstall script that’s fast and free to run.

    Deploying these checks early helped me catch several issues in real projects: packages that were re-published with different contents, accidental commits that updated lockfiles to point to different resolved URLs, and packages that added postinstall miners. The core pattern is simple and powerful: never execute unknown code until you verify the bits you will execute, and make your builds reproducible so divergences are easy to detect.


    You should also check the following news:

    Guides

    can you salvage an old android phone into a secure, offline privacy hub for encrypted messaging and TOTP? a step-by-step rebuild

    25/07/2026

    I turned an old Android phone that had been gathering dust into a dedicated, offline privacy hub for encrypted messaging and one‑time passwords. I...

    Read more...
    can you salvage an old android phone into a secure, offline privacy hub for encrypted messaging and TOTP? a step-by-step rebuild