Skip to main content

Pin, audit, repeat: a practical dependency security checklist

Christina Hill
Christina HillMarketing Manager
12 min read
Pin, audit, repeat: a practical dependency security checklist

Why your package stack deserves production trust

A JavaScript app rarely depends on the few package names you typed into package.json. It depends on their dependencies, and those dependencies pull in their own dependencies, and the chain keeps going until the tree looks more like a family reunion than a tidy list. By the time a build runs, the code that lands in your app may have passed through dozens or hundreds of packages you never opened, never reviewed, and probably couldn’t name from memory.

That matters because package installs and build steps can do more than download files. They can run scripts. They can reach out to the network. They can touch the file setup write artifacts, and sometimes see whatever your pipeline has available at that moment. If a CI job has access to deployment tokens, signing keys, preview-site credentials, or webhook secrets, then a package with install-time code gets a very friendly seat at the table. Not the sort of guest you want wandering into production, but there it is.

If a package can run during install, it can often reach the same environment your build and deploy steps trust.

This comes up in the ordinary, unglamorous workflows builders use every day. A static site still has a build pipeline. Arguably, a Jamstack app still installs packages before it ships. Js project still pulls in build tools, lint rules, and helper libraries that can execute during setup. A Next.js project still pulls in build tools, lint rules, and helper libraries that can execute during setup. Even a simple form backend flow, where you send submissions to email, webhooks, Zapier, Slack, or Google Sheets, usually sits inside a wider deployment chain with its own credentials and automation. The fact that the site’s static doesn’t make the surrounding pipeline static.

So that’s the part teams miss when they think about security only inside their own repo. The form handler may be tiny. The site may be plain HTML. The risk often lives one layer down, in the packages that assemble, test, and deploy the thing. A typo in a package name, a compromised maintainer account, a shady postinstall script, or a dependency that changes ownership can all land before your app ever starts serving users.

This is why package security deserves the same treatment you’d give any other code that can reach production. A new dependency should earn trust. An update should be checked, not waved through because the version number looks harmless. Even a routine npm audit is only part of the picture, since it catches some known issues but won’t spot every weird install script or impostor package. The point of this dependency security checklist is to make that review repeatable instead of dramatic. Pin the versions you ship. Check updates before they land. Treat each new package like code that can touch secrets, artifacts, and the rest of your build system.

Once that frame’s in place, the rest gets a lot easier to reason about.

Pin everything that ships

the next move is boring in the best possible way: make the tree stop changing behind your back, once you accept that your dependency tree can reach into build time. That starts with lockfiles. If your project has a package-lock.json, npm can resolve the same dependency graph on your laptop, along with in CI and during deployment instead of rethinking the whole thing on every install. The npm package-lock.json documentation spells out how that file records the exact package versions and the tree they form, which is exactly what you want when you’re trying to make a build repeatable rather than imaginative.

A lockfile doesn’t make packages safe. It makes their behavior predictable.

That predictability matters because registry changes don’t wait for your release schedule. A dependency that worked fine on Tuesday can pick up a new minor version on Wednesday, and if your range’s loose enough, your next install may decide to be helpful in all the wrong ways. For packages that sit close to your app logic, your build pipeline, or your deployment path, exact versions or very narrow ranges are usually the safer choice. ^1.2.3 is convenient until 1.3.0 arrives with a change you never reviewed. A pinned 1.2.3 is less glamorous, sure, but it gives you a clear point to inspect before anything moves (believe it or not).

That rule applies far beyond the packages you import in your app code. Build tools deserve the same treatment. Maybe, so do linters, compilers and bundlers as well as the little automation pieces that sit in .github/workflows/ and run with your repo’s permissions. A GitHub Action pulled from a broad tag is still a moving target, even if it looks tidy in YAML. Pinning workflow actions to a full commit SHA is slower to set up, but it keeps a maintainer’s future release from silently changing what runs in your pipeline. The same goes for any script that installs tools during CI or deploys artifacts after the fact. If it ships code, it should be pinned like code.

There’s also a quieter perk here: package pinning makes review work less annoying. When version ranges wander, a single npm install can touch half the tree and bury the real change you meant to ship. Diffs stay small and obvious, when versions are fixed. You can see whether a package update changed one module or dragged in a batch of transitive dependencies with it. That matters because the problem often isn’t the package you typed into package.json. It’s the layer underneath it, and the layer underneath that, and the next one after that.

If you want a sanity check while you’re tightening the bolts, package provenance can help you confirm where a package came from and how it was published. Npm documents that too in its package provenance guide. Provenance won’t replace review, but it gives you a cleaner picture of what you’re trusting before the install runs.

A smaller direct dependency list helps as well. Every package you add creates another path for updates, ownership changes and install scripts as well as transitive dependencies to enter the tree. That doesn’t mean “never add anything,” because that’s a good way to reinvent left-pad with tears in your eyes. It does mean you should ask whether a package really earns its spot. If a one-line helper can replace a sprawling utility, the smaller choice usually wins. Fewer direct dependencies mean fewer top-level decisions, fewer update prompts, and fewer places for a problem to start.

For static sites, Jamstack apps, and form-heavy projects, this is easy to forget because the app can feel lightweight. The repo’s small. The frontend’s static. The form backend might be something like Slapform, sitting off to the side and doing its job quietly. Maybe, yet the build still runs through npm, the deploy still passes through CI, and the automation still has secrets in reach. Pinning keeps that machinery from shifting every time the registry sneezes.

So lock the tree, pin the moving parts, and make sure the code you ship is the code you reviewed. The next step is to inspect new packages before they land, because even a perfectly pinned dependency can still be a bad idea.

Audit new dependencies before they land

Pinning gives you a stable target. Auditing tells you whether the target’s worth trusting in the first place.

From there, a lot of dependency reviews stop at the version number. That’s too shallow. A package can have a tidy semver range and still be a bad fit for your build because it was updated once three years ago, changed hands last month, or arrived with a pile of install-time behavior you didn’t ask for. In JavaScript supply chain security, the package itself matters, but so do the people behind it, the release pattern, and everything it pulls in behind the curtain.

And Start with maintenance. Check whether it has recent releases, open issues that get replies, and a history that looks steady rather than erratic, when you’re about to add a package. One release every few months isn’t automatically better than one release every few weeks, but a total silence trail is worth a closer look. Ask whether you actually need it, if the package solves a tiny problem and hasn’t moved in years. A smaller dependency graph is easier to reason about and easier to remove later if things get odd.

Then look at ownership and publication history. Roughly, a sudden transfer to a different maintainer, a new publisher on a familiar package, or an ownership change with no explanation can be benign, but it can also be the sort of thing you’d want to notice before it runs in CI. The same goes for packages that appear to have very little code of their own but an unexpectedly large dependency tree. That usually means more places for risk to enter and more chances for a typo in one transitive package as well as more code you didn’t read but still execute.

Audit new dependencies before they land

A package that looks harmless on the registry can still ask for far more access than its job requires.

Install scripts deserve special attention. In npm, packages can run lifecycle hooks during installation, and the npm install command docs spell out that installation isn’t always just file copying. That matters because preinstall, install, and postinstall hooks can run before your app ever starts. That might be legitimate, if a package needs a build step. And it still tries to execute code during install, I’d want to know why (at least in most cases), if it doesn’t.

This is where automated scanning helps, but only up to a point. Vulnerability checks can catch known CVEs, and they’re useful as a first pass. They don’t catch everything, and typosquatting still slips through. So does a package that was clean yesterday and pushed something nasty in a later release. A green score in a dashboard isn’t a clean bill of health. It’s a starting line.

It also helps to read the package name with a mildly suspicious squint. That’s the sort of mistake attackers count on, if you meant lodash and got loash or loadash. These look like boring misspellings, which is exactly why they work. The same caution applies to packages with names that mirror popular tools but live under a different owner. When the name seems just close enough to fool a tired reviewer, slow down. That’s worth a look too. Npm’s provenance statements documentation explains how a published package can be tied back to a build process, if a package offers provenance information. That doesn’t make the package safe by itself, but it can help you confirm that a release came from the source and workflow you expect. Think of it as one more signal, not the final answer.

The simplest habit is this: before you approve a new dependency, treat it as if it were asking for access to your build environment. Your CI tokens, your deployment artifacts, and whatever secrets live nearby. That’s not far from the truth, because during install. Don’t hand them to a package you haven’t looked at, if you wouldn’t hand those keys to a random script in your repo.

That mindset usually leads to a few practical questions. Does this package need to exist at all? Does it have a smaller alternative? Why does it depend on half the registry to do one tiny job? Those questions don’t sound glamorous, which is probably a good sign. Boring review habits tend to age better than clever ones, and the next step is making sure the environment they run in doesn’t hand extra power to anything you did approve.

Lock down installs, CI, and deployments

The awkward part of dependency security’s that a package doesn’t need to be malicious to cause trouble. It just needs room to move around. Cloud credentials, signing keys, or a blob of artifacts from a previous job, then a compromised package has a lot more to work with than most teams realize, if your install step can read deployment tokens. That risk shows up fast in JavaScript workflows because installs, builds, and deployment scripts often run in the same pipeline, on the same runner, with the same environment variables sitting there like unattended snacks.

Treat the build machine like a temporary production guest: it should get the access it needs, and nothing else.

A cleaner setup separates build-time permissions from deploy-time permissions. The job that runs npm install or pnpm install usually doesn’t need write access to production. It may not need your deployment token at all (for better or worse). Give the build job enough access to compile, test, and package the app, then hand the finished artifact to a separate deploy step that has only the credentials required for publishing. If the build runner is compromised, the blast radius stays smaller. M. And trying to remember why half the variables were shared across three different services.

Least privilege matters inside the install environment too. A dependency install should run with the bare minimum network and filesystem access it needs. If your CI runner can reach internal services, staging APIs, or a production artifact bucket during npm ci, that’s too much power for a step whose main job is to fetch code from the registry. Keep CI secrets out of the install path when you can. Use a dedicated environment for dependency resolution, and avoid passing long-lived tokens into jobs that don’t actually need them. A package with package vulnerabilities is annoying. A package with package vulnerabilities plus access to your build credentials is a different sort of problem entirely.

Lifecycle hooks deserve a close look as well. preinstall, install, and postinstall scripts can execute before your app ever starts, which means they get a chance to run in the middle of setup when people are least likely to notice them. That does not make every hook suspicious. Plenty of legitimate packages use them for small bits of setup. Still, they are worth reviewing in the same way you would skim a shell script before pasting it into a prod terminal. If a dependency downloads binaries, patches files, phones home, or spawns extra processes during install, ask whether you actually want that behavior in CI. Tools like npm audit can help surface known problems, and the npm command docs are worth keeping handy if you want a routine check in your pipeline. For lockfiles and shrinkwrap-based installs, the npm shrinkwrap documentation is useful when you need a fully pinned tree that behaves the same way across environments.

Static-site workflows need the same scrutiny, even though they often feel simpler than full backend apps. Js or Hugo build that sends form submissions to a service like Slapform, or a Jamstack deploy that triggers webhooks to Slack, Zapier, or a build notification tool, still touches external services and delivery tokens. Those tokens often live in the same environment where packages are installed and builds are run. That’s the part people miss. A static site may not have a server sequence of its own, but the build pipeline can still reach plenty of sensitive material. If a package runs code during install, it may be able to read webhook URLs, deployment keys, or any other secret that the pipeline exposes too early.

The practical move is simple enough. Keep install jobs boring, and keep their permissions narrow. Don’t let dependency installation sit next to secrets it doesn’t need. Review package lifecycle scripts before they run in CI. Separate the job that fetches and tests code from the job that publishes it. Once you set that up, a compromised dependency has far less to grab, and your build stops acting like an open drawer with credentials in it (which is worth thinking about).

Make dependency security a repeating habit

Still, once the build’s locked down, the next problem is usually boredom. That’s not a joke, by the way. The safest dependency management process is rarely the one that feels exciting. It’s the one people can repeat on a Tuesday afternoon without needing a heroic mood or a fresh cup of panic.

This means a good routine starts with scheduled updates. Pick a cadence that fits the team, whether that’s weekly, every two weeks, or once a month. Then batch the work. Pull in dependency updates together, run your tests, scan for advisories, and review the diff while the context is still fresh. If you only touch packages when a build breaks, you end up doing security work under pressure, which is usually when people click the wrong thing or accept a risky change just to get the deploy moving again.

That same habit helps with static site security too. Jamstack projects can look small from the outside, but the toolchain often includes bundlers, deployment plugins, webhook helpers, and a few packages nobody remembers adding. A monthly cleanup pass keeps that stack honest. Remove libraries you no longer use. Delete old build helpers that were added for a one-off migration and then left behind like a chair in the hallway (and yes, that matters). Every package you can remove is one less thing to maintain, scan, and trust.

The healthiest dependency tree is the one your team can explain without opening the registry in a cold sweat.

Alerts do their part here, but they work best as a nudge, not a replacement for review. Set up dependency alerts through GitHub, npm, Renovate, or whatever your team already watches, then pair those alerts with periodic audits. That way you catch regressions when a package gets hijacked, a maintainer account changes hands, or a new transitive dependency shows up with a weird install script. Automated tools can spot a lot. But they can’t tell you whether a package still makes sense in your app, or whether it quietly grew six new dependencies just to do the same job it did last quarter.

When an update lands, verify it in a controlled branch or preview environment before it reaches production. Not ideal. For static sites, that usually means a preview deploy or a staging branch with the same build steps as the real site. If a package update changes the bundle, breaks a webhook handler, or alters a form submission flow, you want to see that in a sandbox, not after a customer has already sent you a message that never arrives. Small checks here save a lot of head-scratching later.

There’s a simple rule that holds up well: keep the stack small, keep the versions pinned, and review changes with the same care you’d use for production code. Js app, a Hugo site, or a plain HTML form wired to Slapform. In practice, dependency security’s less about grand policy and more about routine housekeeping. Trim the tree, and read the diff. Ship the update. Repeat.

Newsletter

Stay in the loop

Join our newsletter and get resources, curated content, and inspiration delivered straight to your inbox.