All Docs
FeaturesCalmony PayUpdated March 15, 2026

SEC-26: Why Committing a Lock File Matters

SEC-26: Why Committing a Lock File Matters

Security Control: SEC-26
Introduced in: v1.0.61
Severity: High — affects build reproducibility, supply chain integrity, and automated vulnerability patching


Background

Calmony Pay's CI pipeline was running npm install without a committed lock file (package-lock.json). This is a common but high-impact oversight in Node.js projects. This post explains the risks and the exact steps taken to remediate them.


What is a lock file?

A lock file records the exact resolved version of every dependency (and transitive dependency) at the time npm install was last run. When a lock file is present and npm ci is used, every environment — developer laptop, CI runner, production container — installs byte-for-byte identical packages.

Without a lock file, npm resolves dependencies fresh on every install using the semver ranges in package.json. That means ^1.2.3 could resolve to 1.2.3 today and 1.9.9 tomorrow.


Risks Identified (SEC-26)

1. Non-deterministic Builds

Semver ranges allow npm to install any compatible version. A dependency that is 1.2.3 locally might be 1.2.7 in CI and 1.3.0 in a production image — all within the same ^1.2.3 range. Bugs and regressions introduced by a dependency update may only appear in one environment, making them extremely difficult to diagnose.

2. Supply Chain Attacks

Without a pinned lock file, there is no cryptographic record of the expected package content. Two classes of attack become viable:

  • Dependency confusion — a malicious package with the same name as an internal package, published to the public registry, can be resolved by npm.
  • Typosquatting — a package with a name visually similar to a legitimate dependency (e.g. lodahs vs lodash) can be installed silently.

A committed lock file, combined with npm ci, means npm will refuse to install any package not already recorded — it will error rather than resolve.

3. Dependabot Cannot Function

GitHub's Dependabot scans lock files to understand the current dependency tree and open PRs to update vulnerable packages. With no lock file in the repository, Dependabot has nothing to scan and no manifest to update — automated vulnerability patching via pull requests is effectively disabled.

4. Unreliable npm audit

npm audit reports vulnerabilities against the resolved dependency tree. If CI is running npm install without a lock file, the resolved tree at audit time may differ from the tree installed in a production deployment. This means the audit could report a clean result while production is running a vulnerable version, or vice versa.


Remediation Steps

Step 1 — Generate the lock file locally

npm install
git add package-lock.json
git commit -m "chore: commit package-lock.json for reproducible builds"

Step 2 — Switch CI from npm install to npm ci

In .github/workflows/ci.yml, change the install step:

# Before (non-deterministic)
- name: Install dependencies
  run: npm install

# After (reproducible, enforces lock file)
- name: Install dependencies
  run: npm ci

npm ci will error if package-lock.json is absent or out of sync with package.json, providing a hard guarantee that CI and production use identical packages.

Step 3 — Enable the Node.js cache

In the setup-node action, enable dependency caching to keep CI fast:

- name: Set up Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'   # Add this line

With cache: 'npm', the runner caches ~/.npm and restores it on subsequent runs, so npm ci only fetches packages that have changed.


Result

Once these changes are in place:

  • Every environment installs the same resolved packages.
  • Dependabot can open automated update PRs against the lock file.
  • npm audit results in CI accurately reflect the production dependency tree.
  • npm ci will fail loudly if someone modifies package.json without regenerating the lock file, preventing drift.

References