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.
lodahsvslodash) 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 auditresults in CI accurately reflect the production dependency tree.npm ciwill fail loudly if someone modifiespackage.jsonwithout regenerating the lock file, preventing drift.