Curious Pandas

Trusting what you ship

You chose a handful of packages and installed hundreds. Five real ways the code you install stops being the code you meant - and why every defence is blind to at least one of them.

17 min+40 XP for finishing

This site asks for 8 packages by name in its package.json. Installing them puts 346 packages on disk. You chose eight; strangers chose the other 338, and their dependencies chose some of those. Every one runs with your permissions when you build.

That’s what “supply chain” means here: everything between a stranger’s keyboard and your running code, and every place along that path where the code can quietly become something else.

Three times it really happened

When What How it got in
2017 crossenv, a lookalike of the popular cross-env, sent your environment variables to the attacker’s server. npm removed about 40 packages from the same publisher. You typed the wrong name
2018 event-stream: a new maintainer took over the package and added a dependency, flatmap-stream, whose payload targeted a Bitcoin wallet app. An update you didn’t read
2024 xz utils 5.6.0 and 5.6.1: the line that switched the backdoor on existed only in the released tarballs, not in the git repository. It was found by an engineer chasing odd ssh behaviour, who measured logins taking about 0.8s instead of 0.3s. The artifact differed from the source

The xz one deserves a second read. The repository held the payload only as two innocent-looking binary test files. The line that unpacked them into the build lived in the tarball - the thing people actually installed - and nowhere in the repository. Reviewing the source would not have shown you the switch.

Visual guideWhere a package can change on its way to you
Where a package can change on its way to youA package goes from its source repository, through a build, to a registry, to your install. Provenance attests which source and build produced it. Your lockfile pins the exact version and hash you first installed.Source repowhat reviewers readBuildmakes the tarballRegistryor a mirrorYour installnpm ciProvenancesigned: built hereLockfileversion + hashgit tagpublishdownloadattestsmust matchWhere a package can change on its way to youA package goes from its source repository, through a build, to a registry, to your install. Provenance attests which source and build produced it. Your lockfile pins the exact version and hash you first installed.Source repowhat reviewers readBuildmakes the tarballRegistryor a mirrorYour installnpm ciProvenancesigned: built hereLockfileversion + hashgit tagpublishdownloadattestsmust match
  1. A package travels from source, through a build, to a registry, to you. Every hop is a place it can change.
  2. xz changed here: the release tarball was not what the tagged source would build. Reviewing the source cannot see this hop.
  3. Provenance is a signed record of which source and which build produced the artifact. No record from the expected build, no install.
  4. A compromised mirror or a stolen publish token changes what the registry hands out - same name, different bytes.
  5. Your lockfile pins the exact version and hash you first installed. Different bytes, and npm ci stops.

Install day

Five incidents, modelled on the shapes above, against a toy project. Every package name starts with toy- - real names in a lesson about backdoors would accuse real maintainers. Switch controls on and off and read why each one does or doesn’t help.

Install daytoy packages, real failure shapes
What is going wrong upstream

Your controls
Pick a problem and your controls, then Install.

      What each control actually buys

      A lockfile with npm ci. package-lock.json records the exact tree that was installed, and for each package an integrity field: a sha512 hash of the artifact. npm ci installs exactly that, and if the lockfile and package.json disagree it exits with an error instead of “fixing” anything. It never writes to either file. So bytes can’t change under you - but the lockfile hashes whatever you had when you locked. Lock a backdoored release and you’ve pinned the backdoor.

      Pinning also has a cost you saw in the widget: a lockfile turns “the attacker’s version arrives today” into “arrives at your next routine update”. That’s a delay, not a defence. Security fixes arrive the same slow way, which is the trade-off the tags and digests lesson describes for images.

      Dependency review. A person, or a tool such as a dependency-review check on pull requests, looks at every package name that’s new in the diff. It’s the only control here that notices you asked for the wrong thing. It’s blind to bytes changing under a name you already approved.

      Provenance and signatures. SLSA defines provenance as a record of “what entity built the artifact, what process they used, and what the inputs were”. Its levels climb from “package has provenance” (easy to forge) to builds where forging it means exploiting a vulnerability. Sigstore makes signing practical without key management: a short-lived certificate is issued for the signer’s OIDC identity, the private key is discarded after one signature, and the record goes into a public append-only log, Rekor. Verifying then means checking that the identity in the certificate is the one you expected - for example, the project’s own release workflow.

      Provenance only covers packages that publish it, and most of a real tree doesn’t yet. And it proves where something came from, not that it’s good: a typosquat can have flawless provenance from the attacker’s own repository.

      An SBOM - a software bill of materials - blocks nothing. It’s the list of what’s inside each thing you ship. Its value shows on the morning a new vulnerability is announced: with SBOMs you can answer “are we affected?” by searching, instead of by guessing. That’s the same job the Trivy scanner does continuously in the policy and scanning lesson.

      Check yourself+10 XP

      A release tarball contains code that isn't in the project's git repository, and the reviewed source is clean. Which control can catch it?

      Check yourself+10 XP

      Your team uses lockfiles and npm ci everywhere. A maintainer's account is taken over and a malicious patch release is published. Are you safe?

      Check yourself+10 XP

      Someone adds 'toy-cross-envv' to package.json by mistake. The package is properly signed and has valid provenance. What catches it?

      Do it for real · your machine+50 XP bonus

      Everything above ran in your browser. Now do it on a real cluster withthe tilt platform. You need Docker, kind and Tilt, about 15 GB of free disk and 8 GB of spare RAM. Setup guide · Can my machine run it?

      Two checks you can run today. First, any Node project with a lockfile, including this site’s repository:

      npm ci                  # refuses to "fix" a lockfile that disagrees with package.json
      npm audit signatures    # verifies registry signatures and provenance attestations

      Then, on the tilt platform, see how many images your cluster runs by a movable tag versus a fixed digest:

      kubectl get pods -A -o jsonpath='{range .items[*].spec.containers[*]}{.image}{"\n"}{end}' | sort -u > images.txt
      wc -l < images.txt                  # distinct images
      grep -c '@sha256:' images.txt       # pinned by digest

      The count by digest is usually small. That’s the tags-and-digests trade-off at cluster scale: every tag is something that can be repointed under you.

      Self-reported. We can't see your machine, and we don't want to. This is a bonus, never required.

      Sources

      Finished reading?

      Missions and quizzes above earn their own XP. This marks the lesson done.