Content Security Policy (CSP) is an HTTP response header that instructs the browser which resources it is permitted to load for a given page. A correctly configured policy is one of the most effective controls against cross-site scripting (XSS) attacks. A poorly configured one provides no meaningful protection and can give a false sense of security.
This article covers what CSP actually does, the directive syntax you need to understand, the misconfigurations that consistently render policies ineffective, how to test without breaking your site, and how to confirm your policy is being enforced correctly.
What CSP Does
Without a CSP, a browser executing your page will load scripts, stylesheets, images, and other resources from anywhere referenced in the HTML or injected by JavaScript. If an attacker can inject arbitrary HTML into a page (via XSS, a compromised third-party script, or a stored payload) the browser will execute whatever JavaScript that HTML contains.
CSP changes the default. When a policy is present, the browser checks each resource load against the declared policy before executing it. Resources not matching the policy are blocked and optionally reported. The browser, not the server, enforces this restriction. The server only needs to deliver the header.
CSP is a mitigation layer, not a prevention mechanism. It reduces the exploitability of XSS vulnerabilities but does not fix the underlying injection issue. Both the injection vulnerability and the missing CSP should be addressed independently.
Directive Syntax and Source Expressions
A CSP is a semicolon-separated list of directives. Each directive names a resource type and a list of permitted sources:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self'The most commonly used directives are:
default-src: Fallback for any directive not explicitly set. Ifscript-srcis not defined,default-srcapplies to scripts.script-src: Controls JavaScript sources. The most security-critical directive.style-src: Controls CSS sources.img-src: Controls image sources.connect-src: Controls fetch, XHR, and WebSocket endpoints.frame-ancestors: Controls which origins may embed this page in a frame. SupersedesX-Frame-Optionsin modern browsers.base-uri: Restricts the values that can be used in a<base>element, preventing base-tag injection attacks.form-action: Restricts where forms may submit.
Source expression values that appear in directives:
'self': The page's own origin (scheme, host, port).'none': No sources permitted.https://cdn.example.com: A specific origin.https:: Any HTTPS URL (very permissive).'unsafe-inline': Allows inline scripts and styles.'unsafe-eval': Allowseval(),setTimeout(string), and similar.'nonce-<base64value>': Allows a specific inline script identified by a matching nonce attribute.'sha256-<base64hash>': Allows an inline script matching a specific hash.
Common Misconfigurations
A CSP header that is present but misconfigured provides no real protection. These are the patterns we see most frequently in scanned sites.
unsafe-inline and unsafe-eval
Adding 'unsafe-inline' to script-src permits inline script execution, which is exactly what an XSS payload typically relies on. A policy that allows unsafe-inline for scripts does not protect against XSS.
# This policy does not protect against XSS
Content-Security-Policy: script-src 'self' 'unsafe-inline'Similarly, 'unsafe-eval' permits dynamic code execution via eval(). While some frameworks historically required this, most modern build tools produce code that does not need it. If a dependency requires unsafe-eval, it is worth evaluating whether that dependency can be replaced or whether the code can be restructured.
If your codebase has inline scripts today, the path forward is nonces or hashes, not permanently allowing all inline execution.
Wildcard and Overly Broad Sources
A wildcard (*) in script-src allows scripts to be loaded from any HTTP or HTTPS URL. This defeats the purpose of the directive entirely. The same applies tohttps: as a source: it allows loading scripts from any HTTPS host on the internet, which includes attacker-controlled domains.
# These script-src values provide no meaningful protection:
script-src *
script-src https:
script-src 'self' https:Even a specific domain can be a bypass vector if that domain hosts user-controlled content. For example, allowing https://storage.googleapis.com may let an attacker upload a script to Google Cloud Storage and serve it under your policy.
Missing Directives
When script-src is absent, the browser falls back to default-src. When both are absent, scripts are unrestricted. Two directives are frequently omitted even from otherwise reasonable policies:
- base-uri: Without it, a base-tag injection attack can redirect all relative URLs to an attacker-controlled origin.
- form-action: Without it, forms can be submitted to any destination, enabling phishing-style data exfiltration even when script execution is restricted.
Omitting object-src (or failing to set it via default-src) allows Flash and other plugin content, which can execute JavaScript outside the CSP sandbox. Setting object-src 'none' is correct for virtually all modern sites.
Testing with Report-Only Mode
The Content-Security-Policy-Report-Only header delivers a policy to the browser that is evaluated but not enforced. Violations are reported (if you configure a report endpoint) but resources are not blocked. This lets you audit what a policy would affect before enabling enforcement.
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://cdn.example.com;
report-uri /csp-report-endpointCollect reports for a representative period before switching to enforcement. Pay attention to violations involving your own first-party scripts and third-party analytics or widget integrations, as these will break under enforcement if not accounted for in the policy.
The report-uri directive is deprecated in CSP Level 3 in favor of report-to, but report-uri still has broader browser support. Using both together is safe and covers older clients.
Nonces and Hashes
Nonces allow specific inline scripts to execute without broadly permitting all inline execution. The server generates a cryptographically random value for each response and includes it in both the CSP header and the script tag:
# Response header:
Content-Security-Policy: script-src 'nonce-r4nd0mV4lu3A=='
# Script tag:
<script nonce="r4nd0mV4lu3A==">
// This script is permitted
</script>The nonce value must be: at least 128 bits of entropy, base64-encoded, generated fresh per response (not reused across requests), and not predictable. A static nonce that is the same on every request provides no protection.
Hashes work for inline scripts that do not change between requests. The browser computes a hash of the script content and compares it to the declared value:
Content-Security-Policy: script-src 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='Any change to the inline script content (even adding a space) invalidates the hash and blocks the script. Hashes are well-suited for stable third-party widgets or small first-party initialization scripts.
Verifying Your Policy
After deploying a CSP, confirm the header is present on the responses that matter. Verify it with curl, targeting the specific path:
curl -sI https://example.com/ | grep -i content-security-policyCheck that the policy is served on HTML document responses, not just the root path. API endpoints and asset responses that do not serve HTML do not require a CSP, but document responses (HTML pages) all should carry one.
Use browser developer tools to check for CSP violations in the console. Any blocked resource will produce a clear error message with the directive that blocked it and the resource URL that was denied.
Pay specific attention to CDN and reverse proxy configurations. Some CDN edge rules strip or overwrite security headers. Verify the header is present on the response as it arrives at the browser, not just as configured in your origin server.
The Google CSP Evaluator (csp-evaluator.withgoogle.com) can parse a policy and identify known bypass patterns. It is a useful secondary check but does not replace testing against your actual application.
How WebDefect Evaluates CSP
When WebDefect audits a domain, it fetches the root HTML document and all discovered sub-pages and checks each for the presence and quality of a CSP. The evaluation covers:
- Presence of
Content-Security-PolicyorContent-Security-Policy-Report-Only. - Whether
script-srcordefault-srcpermits unsafe-inline, unsafe-eval, wildcards, or scheme-only sources. - Whether
object-srcis set to'none'or restricted via default-src. - Whether
base-uriandform-actionare explicitly set. - Whether the policy is enforcement mode or report-only only (report-only without an enforcement policy is flagged as a finding).
A policy that is present but effectively permissive is reported as a medium severity finding rather than a clean pass, because the header alone does not indicate protection quality.