Content Security Policy (CSP): how it works, how to test it, how to fix it
A CSP header limits what scripts a page may run. Weak directives such as 'unsafe-inline' or wildcards leave cross-site scripting uncontained.
Typical severity: Medium · Checked by NovaPentest module: Content Security Policy Analysis
What it is
Content Security Policy (CSP) is an HTTP response header that tells the browser which sources of content a page is allowed to use. Each directive covers one kind of resource: script-src for JavaScript, style-src for CSS, img-src for images, connect-src for fetch and XHR destinations, object-src for plugins, and default-src as the fallback for any fetch directive that is not set explicitly. When the page tries to load or run something the policy does not allow, the browser blocks it and can report the violation.
CSP is not a replacement for output encoding or input validation. It is a second layer: if an attacker manages to inject markup into your page, a well-built policy stops the injected script from executing or from loading code from a server the attacker controls.
How much protection a policy gives depends on its content, not on its presence. A few source expressions quietly undo most of the benefit. 'unsafe-inline' allows any inline script or event handler to run, which is exactly the form most injected payloads take. 'unsafe-eval' allows strings to be turned into code through eval(), new Function() and similar calls. A bare * or a broad wildcard such as *.example.com allows code from hosts you do not control. A policy that omits object-src or default-src leaves some resource types without any restriction at all.
A strong modern policy is usually built around nonces or hashes: each script the page legitimately needs carries a random, per-response token, and everything else is refused. The 'strict-dynamic' keyword then lets those trusted scripts load their own dependencies without maintaining a long host allowlist.
How it is exploited
CSP matters at the moment another defence has already failed. Suppose a comment field, a search parameter or a third-party widget lets an attacker place HTML into one of your pages. Without an effective policy, that injection becomes cross-site scripting: the attacker's code runs with the same privileges as your own JavaScript, in your origin, in the victim's browser.
From there the attacker can do whatever your front end can do. They can read data displayed on the page, send requests to your API using the victim's session, change account settings, capture what the user types into forms, or rewrite the page to show a convincing fake login prompt. Cookies marked HttpOnly cannot be read directly, but the attacker does not need them when they can act inside the session.
Each weakness the scanner looks for maps to a way around the policy. With 'unsafe-inline' in script-src, an injected script tag or onerror attribute simply runs, so the policy stops nothing. With 'unsafe-eval', any place where the application turns strings into code becomes a path to execution. With a wildcard source, the attacker can host a payload on any allowed domain, including shared hosting or CDN domains that anyone can upload to. Without object-src, legacy plugin content is unrestricted in browsers that still honour it.
'unsafe-inline' in style-src is less severe, since CSS alone cannot run JavaScript, but injected styles can still be used to deface pages, overlay fake content, or in some cases leak data through selectors that load images.
The practical point: a weak CSP does not make a page vulnerable by itself, but it removes the safety net. When an XSS bug appears, and in large applications one eventually does, a strict policy often decides whether it is a harmless report or a real incident.
How to detect it
The Content Security Policy Analysis check sends one GET request to the URL you submit, follows redirects (every hop is re-validated) and reads the Content-Security-Policy header of the final page, so a bare 301 from www to the apex domain is not what gets graded. It runs in full scans and in the free passive scanner at novapentest.com/scan. It raises four kinds of finding. Any directive containing 'unsafe-inline' is reported at Medium, listing the affected directives, so style-src counts as well as script-src. Any directive containing 'unsafe-eval' is also Medium. A source that is a bare * or starts with *. is reported at Low. Finally, a missing default-src or object-src is reported at Low; a missing script-src is only reported when default-src is also absent, since default-src acts as its fallback. If the header is absent altogether, this check stays silent and the separate HTTP headers check reports the missing Content-Security-Policy header at Medium.
The check has honest limits. It reads only the enforcing header, not Content-Security-Policy-Report-Only and not a policy set in an HTML meta tag. It does not flag 'unsafe-inline' when the same directive also has a nonce, a hash or 'strict-dynamic', because modern browsers ignore 'unsafe-inline' in that combination. It flags a missing object-src even when default-src 'none' already covers it. It does not evaluate scheme sources such as https: or data:, nor directives like base-uri or frame-ancestors, and it grades a single page.
To verify by hand, run curl -sIL https://example.com | grep -i content-security-policy and read the last value printed, which belongs to the final response. Check whether script-src or default-src contains 'unsafe-inline', 'unsafe-eval' or a wildcard. Repeat for important pages, since policies often differ per route, and open the browser developer console to see which resources the policy actually blocks.
How to fix it
Fix the policy in order of impact, and roll changes out in report-only mode first so you do not break the site.
1. Deploy a candidate policy as Content-Security-Policy-Report-Only with a report-to or report-uri endpoint, browse the site, and collect violations for a few days before enforcing.
2. Replace 'unsafe-inline' in script-src with nonces. Generate a new random value for every response, send it in the header and put the same value on each legitimate script tag. A strong starting point is: Content-Security-Policy: script-src 'nonce-R4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'. With 'strict-dynamic', scripts loaded by a trusted script are trusted too, so you do not need a long host list. A static nonce is no better than 'unsafe-inline'.
3. Move inline event handlers such as onclick attributes into script files, because nonces do not apply to them. For small fixed inline scripts, a sha256 hash in script-src is an alternative to a nonce.
4. Remove 'unsafe-eval'. Replace eval() and new Function() with JSON.parse or explicit functions, pass functions rather than strings to setTimeout, and check which libraries still require eval.
5. Replace wildcards with explicit hosts, and set a restrictive fallback: default-src 'self'; object-src 'none'. Add frame-ancestors 'self' to control framing.
6. Deal with style-src 'unsafe-inline' last; it is lower risk, but nonces or hashes work for styles too.
7. Switch to enforcement once reports are clean, keep reporting enabled, and set the header on every HTML response, including error pages, for example with add_header Content-Security-Policy "..." always; in nginx when the policy has no nonce, or in application middleware when it does.
Check your own site
Start with the free passive scan: no account needed, and you get an A to F grade covering headers, TLS, cookies, DNS and other configuration a visitor can observe. It does not send attack payloads, so it will not confirm every class of flaw. The full scan, available with a free account, runs the active checks, including the one described on this page.