← 全部新闻
security5 分钟阅读

Why your package.json is a security risk (and how to fix it)

作者:Seaworthy · 2026年4月26日

A red padlock resting on a black computer keyboard, representing cybersecurity and package security

Photo by Markus Spiske on Unsplash

The file you stopped reading

You wrote it once at the start of the project. Since then, every npm install, every Copilot-generated snippet, and every Stack Overflow copy-paste has been quietly editing it on your behalf. Your package.json is probably thirty lines longer than you remember, and at least some of those lines are working against you.

Let me walk through the specific risks I see most often, and what you can actually do about them.


Unpinned version ranges

This is the one everyone knows about and nobody fixes. A typical package.json looks like this:

"dependencies": {
  "express": "^4.18.2",
  "axios": "^1.6.0",
  "lodash": "^4.17.21"
}

The ^ prefix means "install anything up to but not including the next major version." That sounds reasonable until a package publishes a malicious patch release, or until a dependency of a dependency does. It has happened. The event-stream incident from 2018 is the canonical example, but there have been others since.

The fix is not to pin every transitive dependency manually: that way lies madness. The fix is to commit your package-lock.json (or pnpm-lock.yaml) and treat it as source of truth. When you want to upgrade, do it explicitly:

npm update axios
# or, to see what would change first:
npx npm-check-updates

The lockfile pins exactly what gets installed. Without it, two developers on the same project can be running different package trees without knowing it.


Packages you forgot you installed

Run this:

npx depcheck

You will probably find at least two packages that nothing in your codebase imports anymore. Dead dependencies are not just noise. They are attack surface. A package you do not need is a package you do not update, and a package you do not update will eventually have a known CVE sitting quietly in your node_modules.

Delete them. Your install will be faster and your risk surface smaller.


Packages that have been abandoned

npm audit catches packages with disclosed CVEs. It does not catch packages where the maintainer stopped responding to issues three years ago and the last commit was a README typo.

For that, you can check:

npm view <package-name> time.modified

If the answer is "2019" and the package has thirty open issues, you have a decision to make. Either fork it and own the maintenance, replace it with something actively maintained, or inline the few lines you actually need.

I find is-odd and its extended family of single-function packages to be the worst offenders here. There is no good reason to have a dependency for something you could write in four characters.


Credentials in package scripts

This one stings because it is easy to overlook. Custom scripts in package.json sometimes get API keys or secrets baked in:

"scripts": {
  "deploy": "API_KEY=sk-live-abc123 node deploy.js"
}

That value is now in your git history and, if this is an open-source repo, visible to anyone who clones it. Even in a private repo it is available to everyone with read access.

The correct pattern is to read the value from an environment variable that you set elsewhere:

"scripts": {
  "deploy": "node deploy.js"
}

And in deploy.js:

const apiKey = process.env.API_KEY;
if (!apiKey) throw new Error("API_KEY is not set");

Then manage the actual value in a .env file (gitignored), a secrets manager, or your CI platform's secret storage. The package.json should never contain a credential, even temporarily.


The engines field you skipped

Mismatched Node versions cause bugs that look like application errors. Adding an engines field to your package.json makes the expected version explicit:

"engines": {
  "node": ">=20.0.0"
}

This will not enforce anything by default, but it communicates intent, surfaces mismatches in CI, and is respected by some deployment platforms. It takes thirty seconds to add and has saved me at least that many confused debugging sessions.


What automated scanning catches

Running npm audit regularly helps, but it only covers published CVEs. It misses abandoned packages, version range issues, and credentials. A broader static analysis pass, one that checks your whole repo rather than just the dependency manifest, catches things npm audit does not.

The workflow I use now is: npm audit for known CVEs after installs, depcheck periodically to prune dead dependencies, and a full-repo scanner before any release to catch secrets, outdated configs, and anything else that drifted in while I was focused elsewhere.


Your package.json is not a fire-and-forget file. It accumulates risk over time the same way any other part of the codebase does. The difference is that most of the changes happen without you touching it directly, which makes it easy to forget to review.

Seaworthy's dependency hygiene checks flag unpinned versions, abandoned packages, and missing lockfiles as part of its standard repo scan.

本文由 AI 生成,总结了公开可用的来源。

来自 Seaworthy 团队

Seaworthy 会扫描你的代码仓库,发现类似本文提到的各种问题。

安全漏洞、泄露的密钥和配置错误——在部署前捕获。免费运行,无需账户。

$ npx seaworthycode