Skip to content
AgileLogics
7 min read

Content Security Policy

Prerequisites: Link and embed security

A Content Security Policy (CSP) tells the browser which sources of scripts, styles, images, and other resources a page may use. If an attacker manages to inject markup (XSS), a strict CSP stops their script from running. It's defense in depth: CSP doesn't replace proper escaping, it limits the damage when escaping fails.

Delivering a policy

Preferably as an HTTP header:

Content-Security-Policy: default-src 'self'; img-src 'self' https://images.example; object-src 'none'; base-uri 'none'; frame-ancestors 'self'

Or with a meta tag in the head (which doesn't support frame-ancestors, report-uri, or sandbox):

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none'; base-uri 'none'">

Common directives

Directive Controls
default-src Fallback for the *-src directives not listed
script-src JavaScript
style-src CSS
img-src, font-src, media-src Images, fonts, audio/video
connect-src fetch, XHR, WebSocket destinations
frame-src What you may embed in iframes
frame-ancestors Who may embed you
form-action Where forms may submit
base-uri Allowed base URLs
object-src Plugins — set to 'none'

Source values include 'self', 'none', specific origins (https://cdn.example), https:, 'nonce-…', 'sha256-…', 'strict-dynamic', and — to avoid — 'unsafe-inline' and 'unsafe-eval'.

A nonce-based strict policy

The recommended approach: allow scripts only if they carry a random, per-response nonce. Injected scripts don't know it.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-R4nd0mV4lu3'; object-src 'none'; base-uri 'none'">
    <title>CSP demo</title>
    <script nonce="R4nd0mV4lu3">
      document.addEventListener("securitypolicyviolation", e =>
        console.log("blocked:", e.violatedDirective, e.blockedURI));
    </script>
  </head>
  <body>
    <script nonce="R4nd0mV4lu3">console.log("script with the nonce runs");</script>
    <!-- Wrong: no nonce, so the policy blocks these two -->
    <script>console.log("injected script runs");</script>
    <button type="button" onclick="console.log('inline handler runs')">Click</button>
    <script nonce="R4nd0mV4lu3">document.querySelector("button").click();</script>
  </body>
</html>
script with the nonce runs
blocked: script-src-elem inline
blocked: script-src-attr inline

The script with the nonce ran; the script without it and the inline onclick handler were blocked (the browser also prints its own "Refused to execute..." errors in the console).

  • The server must generate a new unpredictable nonce for every response — a fixed nonce like the one in this demo is useless.
  • Move onclick="..." handlers into scripts with addEventListener, and javascript: URLs into real buttons.
  • 'strict-dynamic' lets a trusted (nonce'd) script load further scripts — useful for loaders and tag managers.

Rolling out safely

Start with Content-Security-Policy-Report-Only: the browser reports violations (to a report-to endpoint) without blocking anything. Fix what breaks, then switch to enforcing.

Trusted Types

require-trusted-types-for 'script' makes the browser reject plain strings passed to dangerous sinks like innerHTML — only values created by a policy you define are accepted. It's supported in Chromium-based browsers, Safari 26, and Firefox 148.

Other useful security headers alongside CSP: Strict-Transport-Security, X-Content-Type-Options: nosniff, and Referrer-Policy.

Related developer tools