Why the same-origin policy isn't enough

The web's security model is built on the same-origin policy: code from https://mybank.com should only access https://mybank.com's data, and https://evil.example.com must never be allowed in. In theory, each origin is isolated from the rest of the web. In practice, attackers have found ways to break that isolation, most notably through cross-site scripting (XSS).

XSS bypasses the same-origin policy by tricking a site into delivering malicious code as if it were legitimate content. Browsers trust all code on a page as part of that page's origin, so even a single injected script compromises the user session and exposes private data. The XSS Cheat Sheet documents a representative range of these injection techniques. Content Security Policy (CSP) is a browser mechanism for reducing both the risk and the impact of such attacks.

Building a policy

An effective CSP relies on a few core practices:

  • Use source allowlists to specify what the client may load.
  • Understand available directives and their keywords.
  • Restrict inline code and eval().
  • Test with violation reports before enforcing.

Source allowlists

Browsers cannot distinguish between script that belongs to your application and script injected by an attacker. They will fetch and execute any code a page requests, from any origin. CSP's Content-Security-Policy HTTP header changes this by letting you define an allowlist of trusted sources; the browser then executes or renders only resources from those sources. Even if an attacker finds an injection point, the malicious script won't match the allowlist and won't run.

Consider a page that relies on its own origin and on https://apis.google.com for scripts:

Content-Security-Policy: script-src 'self' https://apis.google.com

Here script-src restricts script execution to the page's own origin ('self') and to https://apis.google.com over HTTPS. Injected code from any other origin triggers a browser error and is never executed.

Console error: Refused to load the script 'http://evil.example.com/evil.js' because it violates the following Content Security Policy directive: script-src 'self' https://apis.google.com
The console shows an error when a script tries to run from an origin not on the allowlist.

Resource directives

CSP provides granular directives for controlling which resources a page may load. These are the resource directives available at CSP Level 2; a Level 3 draft exists but is largely unimplemented in major browsers.

  • base-uri: restricts URLs allowed in the page's <base> element.
  • child-src: lists URLs for workers and embedded frames; for example, child-src https://youtube.com permits YouTube embeds only.
  • connect-src: limits connectable origins for XHR, WebSockets, and EventSource.
  • font-src: specifies origins that may serve web fonts, for instance font-src https://themes.googleusercontent.com for Google Fonts.
  • form-action: defines valid form submission endpoints.
  • frame-ancestors: specifies which sources may embed the current page via <frame>, <iframe>, <embed>, or <applet>; cannot be set via <meta> or on HTML resources.
  • frame-src: deprecated in Level 2, restored in Level 3; falls back to child-src when absent.
  • img-src: defines origins for image loading.
  • media-src: restricts origins for video and audio.
  • object-src: controls Flash and other plugins.
  • plugin-types: limits invocable plugin types.
  • report-uri: specifies a URL for violation reports; not usable in <meta> tags.
  • style-src: limits stylesheet origins.
  • upgrade-insecure-requests: instructs user agents to rewrite HTTP URLs to HTTPS.
  • worker-src: Level 3 directive restricting worker, shared worker, and service worker URLs; as of July 2017 it has limited implementation.

Without a specific directive, the browser loads the resource from any origin. Setting default-src provides fallback values for any unspecified directive ending in -src; for example, default-src https://example.com without a font-src would only allow fonts from that origin. The following directives do not fall back to default-src, so failing to set them means anything is allowed:

  • base-uri
  • form-action
  • frame-ancestors
  • plugin-types
  • report-uri
  • sandbox

Policy syntax

CSP directives are listed in the HTTP header, separated by semicolons, with all sources for a single resource type in one directive:

script-src https://host1.com https://host2.com

For a web app pulling all resources from https://cdn.example.net, without framed content or plugins:

Content-Security-Policy: default-src https://cdn.example.net; child-src 'none'; object-src 'none'

Specifying sources

Modern browsers support the unprefixed Content-Security-Policy header. Older X-WebKit-CSP and X-Content-Security-Policy headers appearing in tutorials are deprecated.

CSP is applied per page; you send the header with every response you want protected. Source lists can match by scheme (data:, https:), by hostname (example.com matching any scheme or port on that host), or by fully qualified URI (https://example.com:443 matching only HTTPS on that host and port). Wildcards are valid only as a scheme, a port, or in the leftmost hostname position: *://*.example.com:* matches all subdomains of example.com on any scheme and port, but not example.com itself.

Four keywords are also recognized:

  • 'none': matches nothing.
  • 'self': matches the current origin, excluding subdomains.
  • 'unsafe-inline': allows inline JavaScript and CSS.
  • 'unsafe-eval': allows text-to-JavaScript mechanisms such as eval.

These keywords require single quotes. script-src 'self' authorizes scripts from the current host, while script-src self would only allow a server literally named self.

Sandboxing

The sandbox directive differs from others: rather than restricting resources, it limits page actions. When present, the page is treated as if loaded in an <iframe> with a sandbox attribute, which can force a unique origin and block form submission, among other effects. Full details on valid sandboxing attributes are in the HTML5 spec.

Setting policy via meta tag

Although the HTTP header is the preferred delivery method, you can set a policy directly in markup using a <meta> tag with an http-equiv attribute:

<meta http-equiv="Content-Security-Policy" content="default-src https://cdn.example.net; child-src 'none'; object-src 'none'">

This approach cannot be used for frame-ancestors, report-uri, or sandbox.

Banning Inline Script and Style

Origin-based allowlists cannot address the most direct XSS vector: inline script injection. When an attacker injects a payload like <script>sendMyDataToEvilDotCom()</script>, the browser cannot distinguish it from a legitimate inline script. CSP closes this hole by forbidding inline script entirely.

That ban covers script bodies, inline event handlers, and javascript: URLs. Script contents must move to external files, and inline handlers like <a ... onclick="[JAVASCRIPT]"> must be replaced with addEventListener() calls:

<script>
    function doAmazingThings() {
    alert('YOU ARE AMAZING!');
    }
</script>
<button onclick='doAmazingThings();'>Am I amazing?</button>

becomes:

<!-- amazing.html -->
<script src='amazing.js'></script>
<button id='amazing'>Am I amazing?</button>
// amazing.js
function doAmazingThings() {
    alert('YOU ARE AMAZING!');
}
document.addEventListener('DOMContentLoaded', function () {
    document.getElementById('amazing')
    .addEventListener('click', doAmazingThings);
});

This refactor aligns with web best practices beyond security: separating structure from behavior improves readability, and external resources cache and compile more efficiently. Moving inline style tags and attributes into external stylesheets is also strongly recommended, as inline styles can be abused for CSS-based data exfiltration attacks.

Temporarily Allowing Inline Code

Adding 'unsafe-inline' to a script-src or style-src directive re-enables inline code. For a more controlled exception, CSP Level 2 supports nonces and hashes.

With a nonce, add an attribute to the script tag:

<script nonce="EDNnf03nceIOfn39fn3e9h3sdfa">
    // Some inline code I can't remove yet, but need to as soon as possible.
</script>

Then reference it in script-src with the nonce- keyword:

Content-Security-Policy: script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa'

Nonces must be regenerated per request and must be unguessable.

Hashes work similarly but require no tag modification. Compute an SHA hash of the script content and list it in the directive. For this script:

<script>alert('Hello, world.');</script>

the policy needs:

Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='

The sha*- prefix identifies the algorithm; sha256-, sha384-, and sha512- are supported. Exclude the <script> tags when hashing, and note that whitespace and capitalization are significant. In Chrome 40+, DevTools will report the correct SHA-256 hash for each inline script on reload.

Eliminating eval() and Friends

Even without direct script injection, an attacker may trick your app into executing text as code. eval(), new Function(), setTimeout([string], …), and setInterval([string], ...) all enable such execution. CSP blocks these by default.

This affects application design in specific ways:

  • Use the built-in JSON.parse rather than eval for JSON parsing. Safe JSON support exists in every browser since IE8.
  • Rewrite string-based timer calls to use inline functions:
setTimeout("document.querySelector('a').style.display = 'none';", 10);

becomes:

setTimeout(function () {
    document.querySelector('a').style.display = 'none';
}, 10);
  ```
  • Avoid runtime inline templating. Many libraries call new Function() to accelerate template generation, which can evaluate malicious text. Some frameworks, like AngularJS's ng-csp directive, fall back to a safe parser. Precompiled templates—for example with Handlebars—are both more secure and often faster than runtime compilation.

If text-to-JavaScript execution is indispensable, 'unsafe-eval' in script-src re-enables it. This is strongly discouraged due to code injection risk.

Violation Reporting and Report-Only Mode

The report-uri directive makes the browser POST JSON violation reports to a designated endpoint:

Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

Violation reports include:

{
    "csp-report": {
    "document-uri": "http://example.org/page.html",
    "referrer": "http://evil.example.com/",
    "blocked-uri": "http://evil.example.com/evil.js",
    "violated-directive": "script-src 'self' https://apis.google.com",
    "original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser"
    }
}

Each report identifies the affected page (document-uri), its referrer, the offending resource (blocked-uri), the directive violated (violated-directive), and the full policy (original-policy).

Before enforcing a new policy, evaluate it with the Content-Security-Policy-Report-Only header. This mode reports violations without blocking resources:

Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

You can send both headers simultaneously—enforcing one policy while testing another. Monitor the reports, fix issues, then promote the tested policy to enforcement.

Common Policy Patterns

Crafting a policy starts with auditing what your application loads. The following examples illustrate decisions for typical use cases.

Social Media Widgets

  • Use Facebook's Like button in its <iframe> form to sandbox it; add child-src https://facebook.com.
  • X's Tweet button requires its external script. Load it from https://platform.twitter.com and set script-src https://platform.twitter.com; child-src https://platform.twitter.com.
  • Other platforms follow similar patterns. Start testing with default-src 'none' and consult the console to identify needed sources.

Combined widget directives look like:

script-src https://apis.google.com https://platform.twitter.com; child-src https://plusone.google.com https://facebook.com https://platform.twitter.com

Strict Lockdown

A banking site can build a maximally restrictive policy starting from default-src 'none'. If all images, styles, and scripts come from https://cdn.mybank.net, XHR data uses https://api.mybank.com/, and frames are site-local only—with no Flash, fonts, or third-party content—the tightest header is:

Content-Security-Policy: default-src 'none'; script-src https://cdn.mybank.net; style-src https://cdn.mybank.net; img-src https://cdn.mybank.net; connect-src https://api.mybank.com; child-src 'self'

HTTPS Only Without Rewrites

A forum admin wanting to force secure channels but unable to refactor third-party software with inline scripts and styles can use:

Content-Security-Policy: default-src https:; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'

Note that https: in default-src is not inherited by script-src or style-src; those directives must specify their own sources.

Specification Status

Content Security Policy Level 2 is a W3C recommended standard. The specification's next iteration, Content Security Policy Level 3, is under development by the Web Application Security Working Group. Updates are discussed on the public-webappsec@ mailing list.