We've all seen the dependency confusion attacks and typosquatted packages. The risk isn't theoretical. A new contributor, or a compromised account, opens a PR. It includes a legitimate bug fix but also adds a single, malicious dependency to `package.json`. Or they modify an existing version range to pull in a poisoned update. CI runs `npm install` and now you're running attacker code in your pipeline.
My main concern is blast radius. Once that malicious package executes in your CI environment, it can:
* Exfiltrate secrets from pipeline environment variables.
* Poison the build artifact itself.
* Use CI permissions to push back to your repository or other internal systems.
* Lay dormant for a later stage.
So, how do you gate this? I treat `package.json` changes with the same suspicion as a shell script.
My current approach for critical repos is a two-stage CI pipeline:
1. **Validation Stage:** A dedicated job that runs on PRs, before any `npm install`. It uses a simple script to diff the `package.json` and flag any new or changed dependencies. This job must pass before any other CI steps that install dependencies can run.
```bash
# Example snippet for a GitHub Actions workflow
- name: Check for new dependencies
run: |
git fetch origin main
NEW_DEPS=$(git diff --no-color origin/main HEAD -- package.json | grep -E "^+.*" | grep -E "("dependencies"|"devDependencies")" | wc -l)
if [ $NEW_DEPS -gt 0 ]; then
echo "New or modified dependencies detected. Manual review required."
git diff origin/main HEAD -- package.json
exit 1
fi
```
2. **Manual Review:** Any PR that adds/changes a dependency requires explicit manual approval from a maintainer before the full CI (with `npm install`) can proceed. This creates a defined break-glass step.
Beyond that, I also enforce:
* **Lockfiles (`package-lock.json`)** are always required and committed. CI fails if it's out of sync.
* **Dependency auditing** (`npm audit`) runs in CI, but that's reactive, not preventive.
* **Read-only registry tokens** in CI where possible, to prevent the pipeline from publishing.
What's your process? Do you rely on tooling like Renovate with strict rules, or do you have a manual review checklist for dependency changes? Specifically, how do you handle the transitive dependency risk introduced by a seemingly safe version bump?
automate, audit, repeat
That two stage pipeline makes sense. I'm new to this stuff so I have to ask: does the validation stage script just check for new lines, or does it actually verify the new package name against some known safe list? Could a malicious PR also update the validation script itself?