← All Articles 📦 Dependency Vulnerabilities High By Nishant Kumar Verma Updated

Loose Version Pinning: How AI Code Creates Supply Chain Risks

Using * or latest in package.json is a supply chain attack waiting to happen. Learn how to pin versions and protect your build.

DEP-008 DEP-009 DEP-010

Quick Answer

Using *, latest, or overly broad version ranges like ^1.0.0 in package.json means any install without a lockfile pulls in whatever the latest matching 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. DEP-008, DEP-009, and DEP-010 describe the three ways this goes wrong.

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 (CNCF summary) - was hijacked, and its malicious versions stayed the newest release for about four hours until patched ones replaced them. Any project with "ua-parser-js": "^0.7.0" that resolved its dependencies fresh in that window, with no lockfile holding the old version, pulled in the infected 0.7.29.

Supply chain attacks through malicious and hijacked npm packages are a recurring, well-documented problem.

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. If the lockfile isn't committed, 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.

Without a lockfile, 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 *, latest or >= as version specifier High - no upper bound
DEP-009 No lockfile (package-lock.json, yarn.lock or pnpm-lock.yaml) High - builds not reproducible
DEP-010 Lockfiles from more than one package manager (for example npm and Yarn) Medium - installs can disagree

A lockfile only helps if the tool you install with reads it. Lovable, for example, keeps a Bun lockfile in your project and tells you to install with Bun outside Lovable. Run npm install instead and npm ignores that file: without a package-lock.json, 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 a pinned package to a new exact version and update the lockfile
npm install axios@latest --save-exact
git add package.json package-lock.json

The critical distinction: npm install keeps the lockfile's versions while they still satisfy package.json, and resolves new versions (rewriting the lockfile) when they don't. 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 pinned package and test before committing
npm install next@14.2.35 --save-exact
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 run through your CI pipeline and wait for you to merge them - so a new version, compromised or not, does not reach production until someone has looked at the change. Your tests are not written to catch malicious code, so that review is the part that protects you. 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

  1. Run npm list --depth=0 to see what's actually installed and at what version. Use those exact version numbers in your package.json.
  2. Commit your lockfile. If you don't have one, run npm install to generate it and commit package-lock.json immediately. If your .gitignore lists it, remove that line.
  3. Replace latest and * with the specific version currently installed. This is a zero-risk change - you're just making explicit what you're already running.
  4. Switch CI from npm install to npm ci. Update your GitHub Actions workflow, Vercel build settings, or Railway config to use npm ci.
  5. Scan automatically: Tools like VibeDoctor's Vibe Check run Trivy on the versions in your lockfile and list known CVEs with the package and installed version, so a version that slipped in through a loose range shows up. 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?

Partly. Vercel and Netlify both run npm install by default, not npm ci. That uses a committed package-lock.json as long as it still matches package.json, but it does not fail the build on a mismatch. Set the install command to npm ci if you want that guarantee. If the lockfile isn't committed, every build resolves versions fresh.

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 and the lockfile, keeps the locked versions that still fit your ranges, and resolves and writes new ones where they don't. 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.

Diagnose your codebase - free

VibeDoctor checks for DEP-008, DEP-009, DEP-010 and 148 other issues across 21 diagnostic areas - security, performance, code quality, and more.

SCAN MY APP →