warming up your workspace

Defending your dependencies, and verifying a package hash yourself

The companion to this is the anatomy of the June 2026 npm attack: attackers published malicious versions of trusted packages by hijacking the pipeline that publishes them. After it, "audit your dependencies" was everywhere as advice. That advice is usually too vague to act on. Here is the concrete version, and one piece of it you can build yourself in ten lines so it stops being magic.

The one idea: trust on first use, then pin

You cannot verify every line of every dependency. What you can do is decide exactly which bytes you trust once, write that decision down, and refuse anything that doesn't match next time. That is what a lockfile is. The whole game is making "what we installed today" and "what we install in CI tomorrow" provably the same bytes.

Everything below is a variation on that single move: pin the bytes, shrink who can change them, and make changes loud.

Verify an integrity hash yourself

Your package-lock.json already contains a hash of every package, in an integrity field that looks like sha512-Abc123...==. npm checks it on install. But it reads as a black box, so let us reproduce it. The format is the literal string sha512- followed by the base64 of the tarball's SHA-512 digest.

import hashlib, base64, sys

def integrity(tarball_path):
    data = open(tarball_path, "rb").read()
    digest = hashlib.sha512(data).digest()
    return "sha512-" + base64.b64encode(digest).decode()

print(integrity(sys.argv[1]))

Download any package tarball (npm pack <name> writes one), run this on it, and compare the output to the integrity value in your lockfile for that exact version. They match byte for byte. That is the entire integrity mechanism: a hash of the published bytes, recorded at install time, rechecked on every install after.

Three details that matter, and they are the whole point:

  • The hash is of the published artifact, not the source repo. This is exactly the gap the attack used. The bytes can be malicious and still hash consistently, so integrity protects you from tampering after publish, not from a malicious publish. It guarantees "the same bad package every time," which is why it is necessary but not sufficient.
  • It only helps if it is pinned. A hash you recompute and re-trust on every install protects nothing. The lockfile's value is that it was written down once, when you chose to trust that version.
  • base64 of the raw digest, not the hex string. Hash the hex and you will get a different value than npm. Same lesson as hashing without the right encoding anywhere else.

The practical checklist

Pin the bytes.

  • Commit your lockfile and install with npm ci, not npm install. npm ci installs exactly the lockfile and fails if package.json and the lockfile disagree. npm install will happily resolve new versions.
  • Pin GitHub Actions to a commit SHA, not a tag: uses: actions/checkout@<40-char-sha>. Tags are mutable; a tag you trusted can be moved to point at new code. A SHA cannot.

Shrink who can change them.

  • Scope CI tokens to the minimum. With OIDC, restrict the registry's trust to a specific repo and workflow and branch, so a token earned by some other workflow cannot publish.
  • Set permissions: explicitly in each workflow to the least it needs (contents: read for most jobs). The default is often far more than a job requires.
  • Avoid pull_request_target unless you fully understand that it runs untrusted PR code with your repository's permissions.

Disable the amplifier.

  • Install with --ignore-scripts where you can. The postinstall hook is what turned a registry compromise into code running on every machine. Many toolchains run fine without lifecycle scripts; allowlist the few packages that genuinely need them.

Make changes loud.

  • Turn on npm provenance / Sigstore. Provenance attaches a signed statement of where and how a package was built, so consumers can check it came from the expected repo and workflow, not just that it hashes consistently. This is the piece that actually targets the malicious-publish gap.
  • Generate an SBOM (software bill of materials) so you have a list of exactly what is in a build, and diff it between releases. A new transitive dependency appearing is something you want to see.
  • Watch for the tells: a patch release that suddenly adds a postinstall, a new maintainer, a dependency that grew a network call. Diff the tarball between versions when something feels off.

What "good enough" looks like

You will not eliminate supply-chain risk; you inherit the security of every publisher in your tree. But you can make yourself a hard target: locked bytes installed with npm ci, actions pinned to SHAs, least-privilege CI tokens scoped per workflow, scripts off by default, provenance on. Each one removes a step the June attack relied on.

The reason to build the integrity check yourself is that security stops being a set of incantations once you can reproduce the mechanism. A hash is just sha512 and base64. A lockfile is just that hash, written down. If you like understanding defenses by rebuilding the primitive underneath them, that is the whole approach of the cybersecurity with Python track.

Sources