| name | npm-security-best-practices |
| description | Expert guidance on securing npm packages, preventing supply chain attacks, and hardening package manager configurations |
| triggers | ["how do I secure my npm dependencies","protect against supply chain attacks in npm","configure npm security settings","disable npm postinstall scripts","block malicious npm packages","set up secure package manager config","prevent dependency confusion attacks","harden npm package installs"] |
npm Security Best Practices
Skill by ara.so — Security Skills collection.
This skill provides expert guidance on securing npm package installations, preventing supply chain attacks, and implementing security best practices for Node.js development. Based on the comprehensive npm-security-best-practices repository by Lirantal.
Overview
The npm ecosystem is a frequent target for supply chain attacks including:
- Shai-Hulud attacks - Worm-like propagation through compromised packages
- Nx incident - Malicious code in postinstall scripts
- event-stream attack - Long-running exfiltration via lifecycle scripts
- Dependency confusion - Attackers publishing malicious packages with internal names
This skill covers configuration, tooling, and practices to mitigate these risks across npm, pnpm, and Bun.
Secure-by-Default Configuration
npm (.npmrc)
Create or update .npmrc in your project root or global config (~/.npmrc):
ignore-scripts=true
allow-git=none
min-release-age=30
Apply globally:
npm config set ignore-scripts true
npm config set allow-git none
npm config set min-release-age 30
pnpm (pnpm-workspace.yaml)
Create pnpm-workspace.yaml in your project root:
minimumReleaseAge: 43200
trustPolicy: no-downgrade
allowBuilds:
esbuild: true
rolldown: true
nx@21.6.4 || 21.6.5: true
strictDepBuilds: true
blockExoticSubdeps: true
trustPolicyIgnoreAfter: 43200
trustPolicyExclude:
- 'chokidar@4.0.3'
Bun (package.json)
Bun disables postinstall scripts by default. To allow specific packages:
{
"trustedDependencies": [
"esbuild",
"sharp",
"fsevents"
]
}
Installation Commands
Secure npm install
npm install --ignore-scripts --allow-git=none
npm ci --ignore-scripts
npm install lodash --ignore-scripts --allow-git=none
Secure pnpm install
pnpm install
pnpm install --loglevel=verbose
pnpm install --ignore-trust-policy
Selective script execution
Use @lavamoat/allow-scripts for granular control:
npm install -g @lavamoat/allow-scripts
npx allow-scripts setup
Example package.json:
{
"scripts": {
"prepare": "allow-scripts"
},
"lavamoat": {
"allowScripts": {
"esbuild": true,
"core-js": false,
"nx>@nx/nx-linux-x64-gnu": true
}
}
}
Hardening npx Execution
npx --no-install create-react-app my-app
npx create-react-app@5.0.1 my-app
npm view create-react-app
npx create-react-app my-app
Security Tooling Integration
npq - Package quality and security checks
npm install -g npq
npq install lodash
npq check lodash
Socket Firewall (sfw)
npm install -g @socketsecurity/cli
npx @socketsecurity/cli npm install
npx @socketsecurity/cli audit
npx @socketsecurity/cli info lodash
Snyk - Vulnerability scanning
npm install -g snyk
snyk auth
snyk test
snyk monitor
snyk code test
Automated Dependency Updates with Cooldown
Renovate Bot (renovate.json)
{
"extends": ["config:base"],
"minimumReleaseAge": "30 days",
"stabilityDays": 30,
"prCreation": "not-pending",
"packageRules": [
{
"matchUpdateTypes": ["major"],
"minimumReleaseAge": "60 days"
}
]
}
Dependabot (.github/dependabot.yml)
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "security"
Use GitHub Actions to enforce cooldown:
name: Dependency Cooldown Check
on:
pull_request:
paths:
- 'package.json'
- 'package-lock.json'
jobs:
check-release-age:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check package age
run: |
# Custom script to verify package age
node scripts/check-package-age.js
Lockfile Security
Prevent lockfile injection
git diff package-lock.json
npm audit
rm package-lock.json
npm install --ignore-scripts
.gitattributes protection
# Prevent merge conflicts from hiding malicious changes
package-lock.json merge=binary
pnpm-lock.yaml merge=binary
Package Health Assessment
Snyk Advisor lookup
curl https://snyk.io/advisor/npm-package/lodash | jq
npx snyk-advisor lodash
Manual verification checklist
const fetch = require('node-fetch');
async function checkPackage(packageName) {
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
const data = await response.json();
const latestVersion = data['dist-tags'].latest;
const versionInfo = data.versions[latestVersion];
console.log(`Package: ${packageName}@${latestVersion}`);
console.log(`Published: ${versionInfo.time || 'N/A'}`);
console.log(`Maintainers: ${data.maintainers?.length || 0}`);
console.log(`License: ${versionInfo.license || 'NONE'}`);
console.log(`Has scripts: ${!!versionInfo.scripts}`);
console.log(`Dependencies: ${Object.keys(versionInfo.dependencies || {}).length}`);
(versionInfo.?.) {
.();
} {
.();
}
}
(process.[]);
Run:
node check-package-health.js lodash
Preventing Dependency Confusion
.npmrc scoped registries
@mycompany:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
registry=https://registry.npmjs.org/
package.json name scoping
{
"name": "@mycompany/internal-lib",
"version": "1.0.0",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
Dev Container Security
.devcontainer/devcontainer.json
{
"name": "Secure Node.js Dev",
"image": "mcr.microsoft.com/devcontainers/javascript-node:20",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
}
},
"postCreateCommand": "npm config set ignore-scripts true && npm config set allow-git none",
"remoteEnv": {
"NPM_CONFIG_IGNORE_SCRIPTS": "true",
"NPM_CONFIG_ALLOW_GIT": "none"
},
"mounts": [
"source=${localEnv:HOME}/.npmrc,target=/home/node/.npmrc,type=bind,consistency=cached"
]
}
Environment Variable Security
Use .env.vault instead of plaintext .env
npm install dotenv-vault
npx dotenv-vault local build
npx dotenv-vault push
echo ".env" >> .gitignore
echo ".env.*.vault" >> .gitignore
Access encrypted secrets
require('dotenv-vault-core').config();
const apiKey = process.env.API_KEY;
Maintainer Best Practices
Enable 2FA on npm account
npm profile enable-2fa auth-and-writes
npm profile get
Publish with provenance (npm 9.5+)
npm publish --provenance
npm pack --dry-run
GitHub Actions OIDC publishing
name: Publish Package
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- run: npm ci --ignore-scripts
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Troubleshooting
Scripts are blocked but package needs them
Problem: A legitimate package requires postinstall scripts.
Solution: Use allowlist approach with @lavamoat/allow-scripts or pnpm allowBuilds:
allowBuilds:
problematic-package: true
npm install @lavamoat/allow-scripts
npx allow-scripts setup
Git dependency is required
Problem: Internal package only available via git URL.
Solution: Use private npm registry instead:
echo "//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}" >> .npmrc
npm publish --registry=https://npm.pkg.github.com
npm install -g verdaccio
verdaccio
Cooldown period blocks urgent security fix
Problem: Security patch released but blocked by min-release-age.
Solution: Temporarily override for specific package:
npm install package@1.2.3 --no-min-release-age
trustPolicyExclude:
- 'package@1.2.3'
Trust policy blocks legitimate package
Problem: pnpm trust policy rejects package downgrade.
Solution: Investigate first, then exempt if safe:
npm view package@version dist.integrity
npm view package@old-version dist.integrity
trustPolicyExclude:
- 'package@version'
False positive from security scanner
Problem: Snyk/Socket reports issue in vetted package.
Solution: Create exceptions with justification:
ignore:
SNYK-JS-LODASH-12345:
- lodash:
reason: 'Prototype pollution not exploitable in our use case'
expires: '2024-12-31'
CI/CD Integration
GitHub Actions security workflow
name: Security Audit
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install with security flags
run: npm ci --ignore-scripts
- name: Audit dependencies
run: npm audit --audit-level=moderate
- name: Check with Socket
run: npx @socketsecurity/cli audit
env:
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_API_KEY }}
- name: Snyk security
Additional Resources