Command Palette

Search for a command to run...

Hectal
PHASE 21Advanced ~31 min· topic 6 of 6

Topic 21.6

Dependencies, CSP and Supply Chain Hygiene

In one line

Most of your app's code was written by strangers, so keep dependencies few, locked and audited (npm audit, lockfiles, npm ci, pinning, controlled install scripts), and add browser-enforced walls: a Content Security Policy that only runs your own scripts, and Subresource Integrity for anything loaded from a CDN.

Think of it like this

Cooking for a party with ingredients from many shops. Your own recipe is careful, but if one supplier's flour is spoiled, everyone gets sick. So you buy from known suppliers, keep the receipts (exactly what you bought, from where), check recall notices, and taste-test anything new. Your node_modules folder is the pantry: a new Vite + React app already has dozens of packages, a real app hundreds, and each one runs with full access to your users' pages.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Supply chain
All the code and tools your app depends on that you didn't write.
Transitive dependency
A package your dependencies depend on. You never installed it directly, but it ships with your app.
Typosquatting
Publishing a harmful package with a name very close to a popular one, hoping for typos.
Lockfile
A file that records the exact version and hash of every installed package, so installs are repeatable.
npm ci
Clean install exactly from the lockfile; fails if it doesn't match package.json.
Install script
A preinstall/install/postinstall command a package runs on your machine when installed.
CSP
Content Security Policy: a response header listing which sources of scripts, styles and other resources the page may use.
Nonce
A random, single-use value in the CSP header and on allowed script tags, so injected scripts without it are blocked.
SRI
Subresource Integrity: an integrity hash on a script or style tag; the browser refuses the file if it doesn't match.

Step by step

01Audit what you have

Run npm audit and read each entry: package, vulnerable range, severity, advisory link and the path that pulls it in. Real output from a Next.js project is long; this is an excerpt.

terminalwhole filebash
$ npm audit
# npm audit report

source-map-js  1.0.0 - 1.2.1
Severity: high
source-map-js allows event-loop denial of service through indexed source-map section offsets - https://github.com/advisories/GHSA-68fv-2mgg-jv7q
fix available via `npm audit fix`
node_modules/source-map-js

...

18 vulnerabilities (1 low, 17 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

$ npm audit --omit=dev     # only what ships to production
...
16 vulnerabilities (1 low, 15 high)

02Lock installs down

Commit the lockfile, pin new dependencies, install with npm ci everywhere but your own machine, and stop install scripts unless a package needs one.

.npmrcwhole filebash
save-exact=true       # npm install x  ->  "x": "1.2.3", not "^1.2.3"
ignore-scripts=true   # don't run dependencies' install scripts
audit-level=high      # npm audit exits non-zero only for high and critical

03Fix a transitive dependency with overrides

When a vulnerable package is deep in the tree and its parent hasn't released a fix, force a fixed version for everyone. Test afterwards: you're running a combination the parent didn't test.

package.jsonwhole filejson
{
  "dependencies": { "react": "19.2.8", "react-dom": "19.2.8" },
  "overrides": {
    "source-map-js": "1.2.2"
  }
}

04Add a CSP, report-only first

Send the header from your host (Vercel/Netlify headers file, nginx, or Next.js headers()/proxy). Watch the console and your report endpoint for violations, fix them (move inline scripts into files, add real API origins), then switch to the enforcing header name.

vercel.jsonwhole filejson
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy-Report-Only",
          "value": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https://images.shop.example; connect-src 'self' https://api.shop.example; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
        }
      ]
    }
  ]
}
Add a CSP, report-only firstdiagram
Rendering diagram…

05Pin CDN files with SRI

Compute the hash of the exact file and put it on the tag. If you can, self-host instead.

terminalwhole filebash
$ printf "console.log('hi');\n" > widget.js
$ openssl dgst -sha384 -binary widget.js | openssl base64 -A
S/E9AczxJ45AfnRY5VLtw/IwaqqEyuZpg/47V2uDmGHkP2qdE/iuDb2BNHoj+IFy

<script src="https://cdn.example.com/widget@1.0.0/widget.js"
  integrity="sha384-S/E9AczxJ45AfnRY5VLtw/IwaqqEyuZpg/47V2uDmGHkP2qdE/iuDb2BNHoj+IFy"
  crossorigin="anonymous"></script>

Try it yourself

  1. 1

    Patch the vulnerable package

    In the audit example, change tiny-date's version to 1.4.2. Predict the output. (One finding left, color-parse; the count says 1.)

  2. 2

    Model an override

    Set the transitive color-parse version to 1.0.1, as an overrides entry would. Predict the report. (Only tiny-date remains, unless you also fixed it in the first task.)

Code & diagrams

React needs no inline handlers, so a strict CSP fits New tab

The `onClick` never appears in the HTML: React listens at the root from JavaScript, so `script-src 'self'` without `'unsafe-inline'` doesn't affect it. The `style` attribute you see is how the browser reflects styles React set through `element.style`, which CSP doesn't restrict. Server-rendered `style="…"` attributes in HTML are different: a strict `style-src` without `'unsafe-inline'` blocks them, which is why some SSR apps allow inline styles or use nonces.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

HTML React wrote: <button style="color: black;">Clicked 0</button>
inline onclick attribute: null
after a click: <button style="color: green;">Clicked 1</button>
How npm audit works, in miniature New tab

Each advisory here is a range `[from, fixed)`. `chart-kit@3.2.0` is past its vulnerable range, but it pulls in `color-parse@0.9.3`, a **transitive** dependency you never chose. That's why audits read the lockfile (the exact tree), not `package.json`, and why `overrides` exist. Real ranges use full semver syntax, including pre-releases.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

high     tiny-date@1.4.1 -> node_modules/tiny-date
moderate color-parse@0.9.3 -> node_modules/chart-kit/node_modules/color-parse
2 vulnerabilities
A nonce-based CSP for Next.js 16 (proxy.ts, from the Next.js docs)tsx
import { NextRequest, NextResponse } from "next/server";

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const isDev = process.env.NODE_ENV === "development";
  const csp = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ""};
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
  `.replace(/\s{2,}/g, " ").trim();

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);
  requestHeaders.set("Content-Security-Policy", csp);
  const response = NextResponse.next({ request: { headers: requestHeaders } });
  response.headers.set("Content-Security-Policy", csp);
  return response;
}

// Next.js reads the nonce from the header and adds it to its own scripts.
// Pages using nonces must be dynamically rendered (a new nonce per request).
// 'unsafe-eval' is only for development: React uses eval there for better error stacks.
What a CSP violation looks like (Chromium console)bash
# Policy: script-src 'self'
# A missed sink rendered: <img src=x onerror="console.log('this would have run')">

Executing inline event handler violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution. Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present. The action has been blocked.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Install without the lockfile in CI

Delete package-lock.json from the repo (or run npm install in CI) so every build resolves ranges fresh.

terminal
$ CI build
── what you'll see ──
Monday's build and Tuesday's build ship different code with no commit in between. If a dependency publishes a bad patch release in that window, your next deploy includes it automatically.

Break #2

Turn on CSP and break the app

Add an enforcing script-src 'self' header to a page that still has an inline <script> (for example an analytics snippet or inline config) in index.html.

terminal
$ Console
── what you'll see ──
Executing inline script violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-' followed by the hash of that exact script), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.

Myth vs fact

Myth

npm audit saying 0 vulnerabilities means my dependencies are safe.

Fact

It only knows reported vulnerabilities. A freshly hijacked package or an unknown bug won't appear. Lockfiles, review and CSP limit what unknown problems can do.

Myth

Every npm audit finding is urgent.

Fact

Many are in build tools that never reach users, or in code paths you don't use. Triage by whether it ships, whether it's reachable, and severity, then fix promptly what matters.

Myth

CSP replaces sanitising and escaping.

Fact

CSP is a second wall. Policies have gaps (allowed hosts with JSONP or user uploads, 'unsafe-inline' left on), and older browsers ignore parts. Fix sinks first.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    'strict-dynamic' lets a script that was allowed by nonce or hash load further scripts, and makes browsers ignore host lists and 'self' in script-src. That's why nonce-based policies don't need to list CDNs, and why the Next.js example works with dynamic chunk loading.

  • ▸

    CSP hashes ('sha256-…') let specific inline scripts run without nonces, which suits static sites (the hash is computed at build time). Next.js's experimental SRI mode pairs build-time integrity hashes with a static CSP.

  • ▸

    Lockfiles store an integrity hash per package (sha512-…), and npm verifies downloaded tarballs against it, so a registry or mirror can't silently swap package contents for a locked version.

  • ▸

    Third-party scripts you load with <script src> (chat widgets, analytics, A/B testing) run with the same power as your own code, including reading localStorage and the DOM. Load as few as possible, load them in a sandboxed iframe or a web worker where the vendor supports it, and list their exact hosts in CSP.

Remember this

  1. 1

    The software supply chain is everything that goes into your shipped app that you didn't write: npm packages (and their dependencies, called transitive dependencies), build tools, CDN scripts, analytics snippets. Supply chain attacks target that chain: a popular package's maintainer account is taken over and a bad version is published; a package with a name one letter off a popular one (typosquatting) waits for typos; an install script (postinstall) runs code on your machine or CI the moment you install. Bugs in honest packages count too: a vulnerable version of a Markdown or sanitiser library is an XSS hole in your app.

  2. 2

    **npm audit** compares the exact versions in your lockfile against the GitHub Advisory Database and lists known vulnerabilities, with severity and the dependency path. npm audit fix upgrades within your allowed ranges; npm audit fix --force may install breaking major versions, so read what it will do. Many findings are in dev-only tools that never reach users (npm audit --omit=dev focuses on what ships); decide case by case, and use overrides in package.json to force a fixed version of a transitive dependency when its parent hasn't updated. Automate it: Dependabot or Renovate opens pull requests for updates, and CI can fail on high-severity findings in production dependencies.

  3. 3

    Lockfiles and pinning. package.json usually holds ranges (^19.2.0 means "any 19.x at least 19.2.0"), so two installs on different days can get different code. The lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock) records the exact version and an integrity hash of every package, transitive ones included. Commit it, review changes to it, and install with **npm ci** in CI and deploys: it installs exactly the lockfile and fails if it doesn't match package.json. Pinning exact versions (save-exact=true in .npmrc) makes upgrades deliberate. Treat install scripts as a risk: npm install --ignore-scripts (or ignore-scripts=true in .npmrc) stops them, at the cost of packages that need a build step; pnpm 10 doesn't run dependencies' install scripts unless you allow them. npm audit signatures checks the registry signatures and provenance attestations of what you installed.

  4. 4

    Content Security Policy (CSP) is an HTTP response header that tells the browser which sources of scripts, styles, images, frames and connections are allowed on your page. It is the second wall against XSS: even if a sink is missed, an injected inline onerror handler or a script from evil.example is blocked. A good policy for a built React SPA (Vite output loads one module script from your own origin and has no inline scripts) is default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none', plus the API origins in connect-src and image hosts in img-src. Avoid 'unsafe-inline' and 'unsafe-eval' for scripts. React suits CSP well: it attaches events from JavaScript (no onclick="..." attributes) and sets styles through the DOM's style object, which CSP allows. Server-rendered apps (Next.js) use a nonce (a random value per response, put in the header and on each script tag) with 'strict-dynamic'. Start with **Content-Security-Policy-Report-Only** to see violations without breaking anything.

  5. 5

    Subresource Integrity (SRI) protects scripts and styles loaded from someone else's server. You add integrity="sha384-…" (a cryptographic hash of the exact file) and crossorigin="anonymous" to the <script> or <link> tag; if the CDN's file changes by one byte, the browser refuses to run it. It only works for files that never change (a specific version URL), not for scripts that update in place, like many analytics snippets. The best option is often to self-host: install the package and let your bundler include it, so it's covered by your lockfile and your CSP's 'self'. Next.js has experimental SRI for its own bundles (experimental.sri).

  6. 6

    Hygiene habits: add dependencies deliberately (check maintenance, downloads, open issues, what it pulls in; sometimes 20 lines of your own code is better); remove unused ones; keep React, your router and your sanitiser current; never run npx some-package from a tutorial without checking the name; protect your own npm account with 2FA and publish with provenance; give CI tokens the least access. Security tooling like npm audit only knows known problems: lockfiles, review and CSP limit the damage of unknown ones.

Explain it without notes

01

What is a software supply chain attack, and give three ways one can reach a React app?

02

What do lockfiles and npm ci protect against, and why pin versions?

03

What is a Content Security Policy, and why does it suit React apps?

04

What is Subresource Integrity, and when can't you use it?

Practice

01

Write a CI job (GitHub Actions) that installs reproducibly, fails on high-severity production vulnerabilities, and builds.

02

Write a starting CSP for a Vite React SPA that calls https://api.shop.example and shows images from https://img.shop.example.

03

You must load a chart library from a CDN. Write the tag safely, and explain the better alternative.

Trade-offs

  • ↔

    Strict pinning and ignore-scripts make builds predictable and safer but create more update work and break packages that need install steps; automate updates and allow-list the few packages that must build.

  • ↔

    A strict CSP is one of the strongest XSS defences but takes effort to deploy (nonces need dynamic rendering, third-party tools need hosts, inline styles may need work); report-only mode makes the rollout safe.

Done when you can

  • I can explain supply chain attacks and transitive dependencies.

  • I can read npm audit output and decide what to fix, including with overrides.

  • I commit lockfiles, use npm ci in CI and control install scripts.

  • I can write a strict CSP for a React app and roll it out with report-only first.

  • I can add SRI to a CDN tag and explain when self-hosting is better.