Quick Answer
Using *, latest, or overly broad version ranges like ^1.0.0 in package.json means your build automatically pulls in whatever the latest published version of a package is - including a version that has been compromised in a supply chain attack. The fix is to pin exact versions and always commit your lockfile. This is what DEP-008, DEP-009, and DEP-010 check for.
The Supply Chain Attack Nobody Talks About
Supply chain attacks against npm packages are not theoretical. In 2021, the ua-parser-js package - downloaded over 7 million times per week - was hijacked and a malicious version was published for several hours. Every project using "ua-parser-js": "^0.7.0" automatically pulled in the malware-infected version on their next install.
According to Snyk's 2024 Open Source Security report, supply chain attacks against npm packages increased by 78% year-over-year in 2023. The GitHub 2024 Octoverse report confirms that dependency confusion and package hijacking are now the #1 attack vector targeting development pipelines.
For vibe-coded apps built with Bolt, Lovable, Cursor, or v0, this risk is amplified. These tools generate package.json files with permissive version ranges by default - because that's the most common pattern in training data. The lockfile often isn't committed, meaning every developer and every CI build resolves dependencies fresh from the npm registry.
What Loose Pinning Looks Like
// ❌ BAD - Loose version pinning opens supply chain doors
{
"dependencies": {
"react": "latest",
"express": "*",
"lodash": ">=4.0.0",
"axios": "^1.0.0",
"next": "~14.0.0",
"stripe": "latest",
"openai": "*"
}
}
Each of these is dangerous for different reasons. latest and * resolve to whatever is newest at install time - with no ceiling. >=4.0.0 allows any version from 4.0.0 upward, including breaking changes. ^1.0.0 allows any compatible minor/patch update within the major version. ~14.0.0 allows patch updates - safer but still mutable.
None of these produce a reproducible build. Two developers running npm install one week apart may install different versions of the same package.
The Three Checks: DEP-008, DEP-009, DEP-010
| Check | What It Detects | Risk Level |
|---|---|---|
| DEP-008 | * or latest as version specifier |
High - completely unpinned |
| DEP-009 | Missing or uncommitted lockfile (package-lock.json / yarn.lock) |
High - builds not reproducible |
| DEP-010 | Outdated dependencies with known CVEs | Critical - active vulnerabilities |
DEP-009 is particularly common in AI-generated projects. Bolt and Lovable export project archives that include package.json but not package-lock.json - because the lockfile is generated at install time, not included in the generated code. When you clone or download a project without the lockfile, every npm install resolves fresh from npm, pulling whatever version satisfies your ranges at that moment.
How to Pin Dependencies Properly
// ✅ GOOD - Exact version pinning for production reliability
{
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0",
"next": "14.2.5",
"axios": "1.7.2",
"stripe": "16.3.0",
"openai": "4.52.2",
"zod": "3.23.8"
}
}
Exact pinning ("18.2.0" rather than "^18.2.0") means every install - on every machine, in every CI environment - uses the exact same code. There are no surprises when a package publisher releases a patch version that breaks your build or introduces a vulnerability.
For devDependencies (linters, formatters, test runners), caret ranges (^) are generally acceptable because they don't ship to production. The strict pinning rule applies most critically to your production dependencies.
The Lockfile Is Your Contract
The lockfile (package-lock.json for npm, yarn.lock for Yarn, pnpm-lock.yaml for pnpm) records the exact version of every package and sub-dependency that was resolved at install time. It is the single most important file for reproducible builds - and it must be committed to your repository.
# Generate a fresh lockfile
npm install
# Verify the lockfile is clean and committed
git add package-lock.json
git commit -m "chore: commit lockfile for reproducible builds"
# In CI, always use npm ci instead of npm install
# npm ci installs EXACTLY what's in the lockfile
npm ci
# Audit your locked dependencies for vulnerabilities
npm audit
# Upgrade specific packages safely and update the lockfile
npm update axios --save
git add package.json package-lock.json
The critical distinction: npm install resolves dependencies and may update versions within your allowed ranges. npm ci installs exactly what's in the lockfile and fails if the lockfile doesn't match package.json. Your CI/CD pipeline (GitHub Actions, Vercel, Railway) should always use npm ci.
Automated Dependency Updates Done Safely
Exact pinning doesn't mean manual updates forever. The right tooling automates safe, reviewed updates:
# Check which packages have newer versions available
npx npm-check-updates
# Interactively choose which packages to update
npx npm-check-updates -i
# Update a specific package and test before committing
npm update next --save
npm test
git add package.json package-lock.json
GitHub's Dependabot and Renovate Bot can automatically open PRs when new versions are available. These PRs go through your CI pipeline before merging - so a compromised package version would fail your tests rather than silently getting deployed. This is far safer than letting latest pull in whatever is newest at build time with no review step.
How to Fix Loose Pinning in Your AI-Generated App
- Run
npm list --depth=0to see what's actually installed and at what version. Use those exact version numbers in yourpackage.json. - Commit your lockfile. If you don't have one, run
npm installto generate it and commitpackage-lock.jsonimmediately. Add it to your.gitignoreallowlist if it's excluded. - Replace
latestand*with the specific version currently installed. This is a zero-risk change - you're just making explicit what you're already running. - Switch CI from
npm installtonpm ci. Update your GitHub Actions workflow, Vercel build settings, or Railway config to usenpm ci. - Scan automatically: Tools like VibeDoctor (vibedoctor.io) automatically scan your
package.jsonfor loose version pinning patterns (DEP-008, DEP-009, DEP-010) and flag each problem with the exact file and field. Free to sign up.
FAQ
Is it safe to use caret (^) versions in package.json?
Caret ranges are common and generally safe for well-maintained packages with good versioning discipline. But they're not as safe as exact pinning because they allow automated minor and patch updates without review. For critical packages like authentication libraries or payment SDKs, exact pinning is the safer default.
What happens if a malicious version of a package gets published?
If your version specifier allows the malicious version (e.g., you use latest or a range that includes it), the next npm install pulls in the compromised code. With exact pinning and a committed lockfile, a compromised version is only installed if someone explicitly runs an upgrade command and reviews the diff.
Does Vercel or Netlify handle lockfiles automatically?
Yes, both Vercel and Netlify use the lockfile when it's present in your repository and run the equivalent of npm ci. But if the lockfile isn't committed - which is common in AI-generated repos - they fall back to npm install with mutable version resolution.
Does npm audit catch supply chain attacks?
npm audit checks your dependencies against the npm security advisory database, which covers known CVEs. It won't catch a brand-new supply chain attack before it's been reported, but it will flag packages with documented vulnerabilities. Run it as part of every CI build.
What's the difference between npm install and npm ci?
npm install reads package.json, resolves a version for each dependency within your specified ranges, and updates the lockfile. npm ci reads the lockfile directly and installs exactly those versions - it fails if there's a mismatch. Use npm ci in all automated environments.