Supply Chain Attacks: Detecting and Preventing Dependency Compromise
Supply Chain Attacks
The Failure
The e-commerce platform uses an internal npm package, @acme/checkout-utils, published to a private npm registry. A researcher discovers that no package named checkout-utils (without the @acme/ scope) exists on the public npm registry. They register [email protected] on the public registry with a postinstall script that phones home.
When a developer runs npm install on a machine where the npm configuration does not properly scope @acme/ packages to the private registry, npm resolves checkout-utils from the public registry (higher version number wins). The postinstall script executes. This is dependency confusion.
In a CI environment without proper registry scoping, the same thing happens. The pipeline installs the attacker’s package, runs the postinstall script, and the attacker receives CI environment variables including the GITHUB_TOKEN.
The Mechanism
Dependency Confusion
npm (and pip, and most package managers) search multiple registries in order. If a private package name is not scoped to the private registry, the package manager might resolve it from the public registry. The attacker publishes a package with the same name and a higher version number on the public registry.
Prevention: use scoped packages (@acme/checkout-utils) and configure the scope to use the private registry exclusively:
# .npmrc in the repo root
@acme:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
Typosquatting
An attacker publishes lodasg (typo of lodash) on npm. A developer misspells the package name in package.json. The malicious package is installed and runs arbitrary code during installation.
Prevention: npm ci with a committed lock file catches this because lodasg will not be in the lock file. But it does not catch the initial mistake when the developer first runs npm install. Code review is the gate for catching typos in dependency names.
Compromised Maintainer Accounts
The maintainer of a popular package has their npm credentials stolen. The attacker publishes a new version with malicious code. The package signature does not change because npm does not yet enforce package provenance universally.
Prevention: dependency scanning tools (OSV Scanner, npm audit, Dependabot alerts) detect known compromises after they are reported. The window between compromise and detection is the risk. Lock files limit the exposure: the malicious version only enters the build when a developer explicitly updates the dependency.
The Implementation
Registry Scoping
# .npmrc committed to each service repo
# HARDENED: Scope internal packages to private registry
@acme:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
always-auth=true
ignore-scripts=true
The ignore-scripts=true line prevents postinstall scripts from running during npm ci. If a dependency needs a postinstall script (native module compilation), add it to an explicit allow list:
{
"scripts": {
"preinstall": "npx only-allow npm"
},
"allowScripts": {
"bcrypt": true,
"sharp": true
}
}
OSV Scanner in the Pipeline
# HARDENED: Cross-ecosystem vulnerability scanning
jobs:
supply-chain-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: OSV Scanner
uses: google/osv-scanner-action/osv-scanner-action@v2
with:
scan-args: |-
--lockfile=package-lock.json
--format=json
--output=osv-results.json
.
- name: Upload scan results
if: always()
uses: actions/upload-artifact@v4
with:
name: osv-scan
path: osv-results.json
- name: Check for critical findings
run: |
criticals=$(jq '[.results[].packages[].vulnerabilities[] | select(.database_specific.severity == "CRITICAL")] | length' osv-results.json)
if [ "$criticals" -gt 0 ]; then
echo "::error::Found $criticals critical vulnerabilities"
jq '.results[].packages[].vulnerabilities[] | select(.database_specific.severity == "CRITICAL") | .id' osv-results.json
exit 1
fi
OWASP Dependency-Check for Java Services
# HARDENED: OWASP Dependency-Check for the inventory service (Java)
jobs:
dependency-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: OWASP Dependency-Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: inventory-service
path: .
format: JSON
args: >-
--failOnCVSS 7
--suppression dependency-check-suppression.xml
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: dependency-check-report
path: reports/
The --failOnCVSS 7 flag fails the scan when any dependency has a CVE with a CVSS score of 7.0 or higher. The --suppression file works like Trivy’s ignore file: documented exceptions with justifications.
The Gate
Three gates protect against supply chain attacks:
- Registry scoping prevents dependency confusion at the
npm installlevel. ignore-scripts=trueprevents malicious postinstall scripts from executing during the build.- OSV Scanner / Dependency-Check detects known vulnerabilities in resolved dependencies.
Gates 1 and 2 are preventive. Gate 3 is detective. Together, they reduce the attack surface to the window between a compromise and its discovery in vulnerability databases.
The Recovery
When a dependency compromise is detected:
Immediate (within 1 hour):
- Pin to the last known good version in the lock file.
- Rotate all secrets that were available to CI runs during the exposure window.
- Check CI logs for signs of exfiltration: unexpected network calls, artifact modifications, API requests to unknown endpoints.
Short-term (within 24 hours): 4. Audit all builds that ran during the exposure window using the GitHub Actions API:
# Find all workflow runs in the exposure window
gh run list --repo acme/checkout-service \
--created "2026-05-20..2026-05-21" \
--json databaseId,conclusion,createdAt
- Check if compromised images were deployed to any environment. If yes, roll back using the GitOps revert (CH1-S1).
- Report the compromise to the package registry.
Long-term (within 1 week): 7. Review all dependency update PRs from the past 30 days for suspicious patterns. 8. Consider whether the dependency can be replaced with a smaller, better-maintained alternative. 9. Add the package to an internal watch list for future auditing.