Content Security Policy and Subresource Integrity solve different problems, and they work best when you treat them as a pair.

CSP answers: what sources am I willing to trust?

SRI answers: is this exact file the one I meant to load?

That distinction matters. I still see teams deploy one and assume they’re covered. They’re not.

If you load a third-party script from an allowed domain in CSP, CSP is happy. But if that third-party file changes unexpectedly, CSP won’t help. SRI will.

If you use SRI without CSP, the browser can verify the file hash, but your page may still allow random inline scripts, unsafe eval, or script injection from places you never meant to trust. CSP fixes that.

Use both.

The short version

  • CSP restricts where resources can come from and how scripts can execute.
  • SRI verifies the cryptographic hash of external JS or CSS files.
  • Together, they reduce supply-chain risk and script injection risk.

What SRI looks like

Here’s the classic example:

<script
  src="https://cdn.example.com/app.8f3c1.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GhEXAMPLEHASH1234567890"
  crossorigin="anonymous"></script>

And for CSS:

<link
  rel="stylesheet"
  href="https://cdn.example.com/app.4a21d.css"
  integrity="sha384-H8BRh8j48xEXAMPLEHASH0987654321abc"
  crossorigin="anonymous">

The browser downloads the file, hashes it, and compares it to the integrity value. If the bytes don’t match, the browser blocks it.

That’s exactly what you want for third-party CDNs and any static asset that should never change unexpectedly.

What CSP looks like

A minimal CSP for a self-hosted app might look like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self';

That policy says:

  • default to same-origin only
  • allow scripts from self and inline scripts only if they carry the right nonce
  • block plugins with object-src 'none'
  • prevent clickjacking with frame-ancestors 'none'

If you want ready-made policy patterns, csp-examples.com is handy for quick starting points.

Why CSP and SRI are not interchangeable

Here’s the real-world split:

CSP protects against:

  • injected <script src="https://evil.com/x.js">
  • inline script injection when nonces or hashes are enforced
  • loading assets from unapproved origins
  • dangerous execution patterns like unsafe-inline and unsafe-eval if you avoid them

SRI protects against:

  • a compromised CDN asset
  • a silently changed third-party file
  • corrupted or tampered static assets in transit or at origin

CSP says “only scripts from cdn.example.com.” SRI says “and only this exact script from cdn.example.com.”

That’s why they fit together so well.

Copy-paste example: CSP + SRI for a CDN script

This is the pattern I recommend when you must load a third-party script directly from a CDN.

HTML

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>CSP + SRI Demo</title>

  <meta
    http-equiv="Content-Security-Policy"
    content="
      default-src 'self';
      script-src 'self' https://cdn.jsdelivr.net;
      style-src 'self';
      img-src 'self' data:;
      object-src 'none';
      base-uri 'self';
      frame-ancestors 'none';
    ">

  <script
    src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"
    integrity="sha384-REPLACE_WITH_REAL_HASH"
    crossorigin="anonymous"></script>
</head>
<body>
  <h1>Hello</h1>
</body>
</html>

Header version

Don’t ship CSP in a meta tag unless you have to. Use the HTTP header.

Nginx

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always;

Apache

Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';"

How to generate an SRI hash

Use OpenSSL:

curl -s https://cdn.example.com/app.js \
  | openssl dgst -sha384 -binary \
  | openssl base64 -A

Then prepend the algorithm:

sha384-<base64-output>

If you already have the file locally:

openssl dgst -sha384 -binary app.js | openssl base64 -A

For build pipelines, I’d automate this instead of hand-generating hashes. Most modern bundlers and asset pipelines can emit SRI attributes during build.

A practical deployment pattern

The cleanest setup usually looks like this:

  1. Self-host your own app bundle
  2. Use CSP nonces for dynamic inline bootstrapping only if necessary
  3. Use SRI for any external CDN-hosted JS or CSS
  4. Keep third-party domains in CSP as narrow as possible

Example:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-abc123' https://cdn.example.com;
  style-src 'self' https://cdn.example.com;
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self';
<script nonce="abc123">
  window.appConfig = { apiBase: "/api" };
</script>

<script
  src="https://cdn.example.com/widget.min.js"
  integrity="sha384-REPLACE_ME"
  crossorigin="anonymous"></script>

That gives you:

  • CSP control over allowed origins and inline execution
  • SRI verification for the third-party widget

Where people mess this up

1. Allowing a wide domain in CSP and skipping SRI

This is the classic “we trust the CDN” mistake.

script-src 'self' https://cdn.example.com

That’s better than nothing, but if the CDN asset changes, you’re trusting whatever is served.

Add SRI.

2. Using SRI on files that change constantly

If your file changes every deploy and you don’t update the hash, the browser will block it. That’s expected.

SRI works best with:

  • versioned filenames
  • immutable assets
  • external libraries
  • CDN-hosted static files

3. Forgetting crossorigin="anonymous"

For cross-origin SRI, you usually need:

crossorigin="anonymous"

Without it, browsers can fail the integrity check or handle the request differently than you expect.

4. Thinking SRI protects inline scripts

It doesn’t.

For inline scripts, CSP nonces or CSP hashes are the right tool:

script-src 'self' 'nonce-abc123'

or

script-src 'self' 'sha256-abc...'

5. Keeping unsafe-inline in script-src

If your script-src still includes unsafe-inline, you’ve kneecapped one of the best parts of CSP.

For scripts, avoid it.

For styles, teams often keep unsafe-inline longer than they should. It’s common, but still worth cleaning up over time.

Real header example

Here’s a real CSP header from headertest.com:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-MWZiMTM1ZmItZGJkNS00OThhLTk4NGMtYmUwYmZkMzc4NWU2' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://app.tallytics.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

A few things I like about it:

  • object-src 'none'
  • frame-ancestors 'none'
  • base-uri 'self'
  • a nonce-based script-src
  • strict-dynamic, which is powerful when you’re managing trusted bootstrap scripts carefully

A few practical observations:

  • style-src 'unsafe-inline' is common in the real world, especially with consent managers and tag tooling, but it’s still a compromise.
  • Analytics and consent platforms expand your CSP fast. That’s normal, but every third-party domain should be there for a reason.
  • This is exactly the kind of setup where SRI can still add value for any stable external scripts you directly include in markup.

Best-practice template

If you want a sane baseline, start here:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';
  upgrade-insecure-requests;

And for external static assets:

<link
  rel="stylesheet"
  href="https://cdn.example.com/lib.min.css"
  integrity="sha384-REPLACE_WITH_REAL_HASH"
  crossorigin="anonymous">

<script
  src="https://cdn.example.com/lib.min.js"
  integrity="sha384-REPLACE_WITH_REAL_HASH"
  crossorigin="anonymous"></script>

My rule of thumb

  • If it’s inline, CSP nonce or CSP hash.
  • If it’s external, CSP allowlist plus SRI.
  • If it’s third-party, be extra suspicious.
  • If you can self-host, do that first.

That last point is boring, but boring security wins. The fewer third parties you depend on, the simpler your CSP gets and the less you need to trust someone else’s deploy pipeline.

Quick checklist

Use this before shipping:

  • CSP sent as an HTTP header
  • object-src 'none'
  • base-uri 'self'
  • frame-ancestors 'none'
  • no unsafe-inline in script-src
  • nonces or hashes for inline scripts
  • SRI on external CDN JS/CSS
  • crossorigin="anonymous" on cross-origin SRI assets
  • third-party origins trimmed to the minimum
  • report-only policy tested before enforcement if you’re tightening an existing app

CSP and SRI aren’t competing controls. They cover each other’s blind spots. That’s why I deploy them together whenever I can.