docs(security): harden package-manager supply-chain guidance

This commit is contained in:
Federico Bartoli
2026-07-11 09:34:05 +02:00
parent 4e8bd9fde4
commit 175013ab41
2 changed files with 58 additions and 29 deletions
+37 -14
View File
@@ -101,24 +101,47 @@ cors({ origin: '*' }) // Allows any origin
## Dependency Security
```bash
# Audit dependencies
npm audit
First locate the **installation boundary**. If the package is matched by a parent `workspaces` declaration, use that workspace root; otherwise use the nearest project root that owns both its manifest and dependency graph. At that boundary, corroborate `packageManager` (when present), the lockfile, and CI commands. Stop if they disagree or competing manager lockfiles exist there. A nested project is independent only when it is outside the parent workspace; independent subprojects may legitimately use different managers.
# Fix automatically where possible
npm audit fix
| Manager/version signal | Frozen/immutable CI install | Known-advisory audit |
|---|---|---|
| npm (`package-lock.json` or `npm-shrinkwrap.json`) | `npm ci` | `npm audit` |
| pnpm | `pnpm install --frozen-lockfile` | `pnpm audit` |
| Yarn 2+ | `yarn install --immutable` | `yarn npm audit -A -R` |
| Yarn 1 | `yarn install --frozen-lockfile` | `yarn audit` |
# Check for critical vulnerabilities
npm audit --audit-level=critical
For an unlisted manager or version, consult its official documentation; do not substitute another manager's commands or newer defaults.
# Keep dependencies updated
npx npm-check-updates
```
### Install-Script Gate
**Supply-chain hygiene** (`npm audit` won't catch malicious packages):
- [ ] Lockfile committed; CI installs with `npm ci` (not `npm install`)
- [ ] New dependencies reviewed (maintenance, downloads, `postinstall` scripts)
- [ ] No typosquats (`cross-env` vs `crossenv`, `react-dom` vs `reactdom`)
Never discover dependency lifecycle scripts by first executing an ordinary install on a client whose defaults have not been verified.
1. Bootstrap with dependency scripts disabled, or with a documented default-deny policy plus fail-closed enforcement.
2. Inspect the exact script source and package version before approval.
3. Record the narrowest native allow/deny policy at the installation boundary and commit it.
4. Run a clean frozen/immutable install with that policy and verify the required packages still build.
| Manager version | Native policy |
|---|---|
| npm 11.18 | Treat unreviewed scripts as allow-with-warning unless `strict-allow-scripts=true` is already enforced; otherwise bootstrap with `npm ci --ignore-scripts`. From the installation boundary, use the workspace-unaware `npm install-scripts ls`, keep approvals version-pinned, and record denials name-wide. |
| Older or unknown npm | Bootstrap with `npm ci --ignore-scripts`. Keep scripts disabled unless the pinned version documents an enforceable policy and each required script has been reviewed. |
| pnpm 11+ | Use `pnpm approve-builds` and commit `allowBuilds` decisions; `strictDepBuilds` defaults to `true`, so unreviewed builds fail. |
| pnpm 10.2610.x | Configure `allowBuilds` explicitly, or use `pnpm approve-builds` with the legacy `onlyBuiltDependencies` / `ignoredBuiltDependencies` lists. Set `strictDepBuilds: true`; its v10 default is `false`. |
| pnpm 10.110.25 | `pnpm approve-builds` records the legacy lists; enable `strictDepBuilds` where supported (10.3+). |
| Older or unknown pnpm | Bootstrap with `pnpm install --frozen-lockfile --ignore-scripts`. Keep scripts disabled unless the pinned version documents an enforceable policy. |
| Yarn 4.14+ | Dependency postinstalls are disabled by default. Grant only required exceptions with top-level `dependenciesMeta.<package>.built: true`. |
| Yarn 24.13 | Set `enableScripts: false` in `.yarnrc.yml`, then grant only required exceptions with top-level `dependenciesMeta.<package>.built: true`; do not enable scripts globally. |
| Yarn 1 | Bootstrap with `yarn install --ignore-scripts`; keep scripts disabled unless each required exception is reviewed under the pinned client's documented workflow. |
These controls are version-sensitive. Verify them against the official [npm install-scripts](https://docs.npmjs.com/cli/v11/commands/npm-install-scripts/) and [install](https://docs.npmjs.com/cli/v11/commands/npm-install/) docs, [pnpm approve-builds](https://pnpm.io/cli/approve-builds) and [build settings](https://pnpm.io/settings#allowbuilds), or [Yarn security](https://yarnpkg.com/features/security) and [manifest](https://yarnpkg.com/configuration/manifest#dependenciesMeta) guidance before changing policy.
**Supply-chain hygiene** (advisory audits do not catch newly malicious packages):
- [ ] Exactly one authoritative lockfile per project/workspace root is committed and CI never rewrites it
- [ ] Critical/high findings are triaged for reachability; deferrals have a reason and review date
- [ ] Forced audit remediation (`npm audit fix --force` or equivalent) is never automatic; remediation diffs and changelogs are reviewed
- [ ] Registry signatures/provenance are verified where the manager supports it
- [ ] Dependency lifecycle scripts are blocked before first execution and approved only through the pinned manager's native policy
- [ ] New dependencies are reviewed for ownership, maintenance, release age, provenance, transitive graph, and typosquatting
## AI / LLM Security
+21 -15
View File
@@ -50,7 +50,7 @@ If you can't name the trust boundaries for a feature, you're not ready to secure
- **Hash passwords** with bcrypt/scrypt/argon2 (never store plaintext)
- **Set security headers** (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
- **Use httpOnly, secure, sameSite cookies** for sessions
- **Run `npm audit`** (or equivalent) before every release
- **Run the detected package manager's native audit** against the committed lockfile before every release
### Ask First (Requires Human Approval)
@@ -269,16 +269,16 @@ function validateUpload(file: UploadedFile) {
}
```
## Triaging npm audit Results
## Triaging Dependency Audit Results
Not all audit findings require immediate action. Use this decision tree:
Package-manager audits report known advisories; they do not prove a package is trustworthy or that vulnerable code is reachable. Use this decision tree:
```
npm audit reports a vulnerability
The native package-manager audit reports a vulnerability
├── Severity: critical or high
│ ├── Is the vulnerable code reachable in your app?
│ ├── Is the vulnerable code reachable in runtime, build, test, or deployment paths?
│ │ ├── YES --> Fix immediately (update, patch, or replace the dependency)
│ │ └── NO (dev-only dep, unused code path) --> Fix soon, but not a blocker
│ │ └── NO (confirmed unused across those paths) --> Fix soon, but not a blocker
│ └── Is a fix available?
│ ├── YES --> Update to the patched version
│ └── NO --> Check for workarounds, consider replacing the dependency, or add to allowlist with a review date
@@ -298,12 +298,16 @@ When you defer a fix, document the reason and set a review date.
### Supply-Chain Hygiene
`npm audit` catches known CVEs; it won't catch a malicious or typosquatted package. Also:
Do not assume npm or treat the nearest manifest as the install root. Apply this order:
- **Commit the lockfile** and install with `npm ci` (not `npm install`) in CI — reproducible builds, no silent version drift.
- **Review new dependencies before adding them** — maintenance, download counts, and whether they truly earn their place. Every dependency is attack surface (OWASP **A06: Vulnerable Components**, **LLM03: Supply Chain**).
- **Be wary of `postinstall` scripts** in unfamiliar packages — they run arbitrary code at install time.
- **Watch for typosquats** — `cross-env` vs `crossenv`, `react-dom` vs `reactdom`.
1. **Find the installation boundary and manager.** Use the workspace root that owns the lockfile, or an independent nested project only when it is outside that workspace. There, corroborate `packageManager` (when present), the lockfile, and CI; stop on disagreement or competing lockfiles. Pin the manager version and use the matrix in `references/security-checklist.md`.
2. **Block dependency scripts before first execution.** Bootstrap with scripts disabled or a documented fail-closed policy, inspect the pending script source, approve only the minimum required packages, commit the policy, then verify with a clean frozen/immutable install. Never blanket-approve scripts.
Audits only find known advisories; they do not catch a newly malicious or typosquatted package. Therefore:
- **Never apply forced audit remediation automatically** (`npm audit fix --force` or equivalent). Preview the remediation, read changelogs, and test each resulting upgrade; forced fixes may cross declared dependency ranges.
- **Verify registry signatures and provenance where supported** (`npm audit signatures`, `pnpm audit signatures`) and treat absence as a signal to investigate, not automatic proof of compromise.
- **Review new dependencies, lockfile diffs, and script-policy changes together** — ownership, maintenance, release age, provenance, transitive graph, and typosquats such as `cross-env` vs `crossenv` (OWASP **A06**, **LLM03**).
## Rate Limiting
@@ -409,8 +413,9 @@ container.textContent = await llm.reply(userMessage);
- [ ] Error messages don't expose internals
### Supply Chain
- [ ] Lockfile committed; CI installs with `npm ci`
- [ ] New dependencies reviewed (maintenance, downloads, postinstall scripts)
- [ ] One authoritative lockfile committed; CI uses that manager's frozen/immutable install
- [ ] Native audit triaged by reachability and fix risk; dependency install scripts blocked unless explicitly approved
- [ ] New dependencies reviewed (ownership, provenance, release age, transitive graph)
### AI / LLM (if used)
- [ ] Model output treated as untrusted (no eval/SQL/innerHTML/shell)
@@ -432,6 +437,7 @@ For detailed security checklists and pre-commit verification steps, see `referen
| "It's just a prototype" | Prototypes become production. Security habits from day one. |
| "Threat modeling is overkill here" | Five minutes of "how would I attack this?" prevents the design flaws no control can patch later. |
| "It's just LLM output, it's only text" | That "text" can be a SQL statement, a script tag, or a shell command. Treat it like any untrusted input. |
| "The audit passed, so the dependency is safe" | Audits match known advisories. They do not detect a newly malicious package or make unreviewed install scripts safe to execute. |
## Red Flags
@@ -441,7 +447,7 @@ For detailed security checklists and pre-commit verification steps, see `referen
- Missing CORS configuration or wildcard (`*`) origins
- No rate limiting on authentication endpoints
- Stack traces or internal errors exposed to users
- Dependencies with known critical vulnerabilities
- Dependencies with known critical vulnerabilities, competing lockfiles at one installation boundary, non-reproducible installs, or blanket-approved scripts
- Server fetches user-supplied URLs without an allowlist (SSRF)
- LLM/model output passed into a query, the DOM, a shell, or `eval`
- Secrets, PII, or the full system prompt placed inside an LLM context window
@@ -450,7 +456,7 @@ For detailed security checklists and pre-commit verification steps, see `referen
After implementing security-relevant code:
- [ ] `npm audit` shows no critical or high vulnerabilities
- [ ] The native audit has no unmitigated reachable critical/high findings; CI preserves the authoritative lockfile and blocks unreviewed dependency scripts
- [ ] No secrets in source code or git history
- [ ] All user input validated at system boundaries
- [ ] Authentication and authorization checked on every protected endpoint