Using Content Security Policy (CSP) to Stop XSS Attacks
#security
#webdev
#csp
#tutorial
Overview
Cross-site scripting (XSS) is a common and impactful web vulnerability that allows attackers to inject malicious scripts into webpages viewed by other users. Content Security Policy (CSP) is a powerful defense-in-depth mechanism that helps prevent many XSS variants by restricting where scripts, styles, images, and other resources can be loaded from. When configured correctly, CSP can stop inline scripts from executing and limit the sources your page trusts.
This post breaks down how CSP works, which directives matter most for XSS, how to implement nonce and hash-based allowances, and how to test and monitor CSP in production.
How CSP helps prevent XSS
- CSP acts as a whitelist: the browser only loads and executes resources from allowed sources.
- Inline scripts can be blocked unless explicitly permitted via nonces or hashes, reducing risk from inline event handlers or injected scripts.
- CSP can report violations to a monitoring endpoint, helping you detect XSS attempts in real time.
- It complements other defenses (escape output, proper input validation) and reduces the blast radius of XSS breaches.
Core directives to consider for XSS protection
- default-src: Fallback directive for other resource types.
- script-src: Controls JavaScript sources. Use ‘self’ and consider nonces or hashes for inline scripts.
- style-src: Controls CSS sources. Use nonces or hashes for inline styles where needed.
- object-src: Restricts plugins like Flash; set to ‘none’ unless required.
- img-src, font-src, connect-src: Scope sources for images, fonts, and network requests.
- form-action, frame-ancestors, base-uri: Additional hardening to limit where data can be sent and where the document can be framed.
A minimal, hardened starting policy might look like:
- default-src ‘self’;
- script-src ‘self’ ‘nonce-
’; - style-src ‘self’ ‘nonce-
’; - object-src ‘none’;
- img-src ‘self’ data:;
- connect-src ‘self’;
- form-action ‘self’;
- base-uri ‘self’;
- frame-ancestors ‘none’;
- report-uri https://example.com/csp-report
Note: The nonce value should be generated per response and never reused.
Nonces and hashes: allowing legitimate inline code
-
Nonce-based approach:
- Generate a unique nonce (a random string) for each HTML response.
- Include the nonce in CSP: script-src ‘self’ ‘nonce-
’. - Attach the same nonce to any inline .
- Pros: You can keep inline scripts while controlling which ones run.
- Cons: Requires per-response nonce generation and careful template management.
-
Hash-based approach:
- Compute a cryptographic hash (SHA-256/384/512) of each inline script, then list the hash in CSP: script-src ‘self’ ‘sha256-…’.
- Pros: No per-response nonce management; strong guarantees for specific script blocks.
- Cons: Any change to the inline script requires updating the hash; less flexible for dynamic inline content.
-
Practical tip: If you use modern frontend frameworks that render scripts, prefer external scripts from trusted origins and minimize or remove inline scripts to simplify CSP management.
Example (nonce): Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘nonce-abc123’; style-src ‘self’ ‘nonce-abc123’; And in HTML:
Example (hash): Content-Security-Policy: script-src ‘self’ ‘sha256-XYZ…‘;
(hash must match the exact inline content)Reporting and monitoring CSP violations
- report-uri (deprecated in CSP Level 3 but still widely supported) and report-to are used to receive violation reports.
- Use a reporting endpoint to gather details about blocked attempts: blocked script source, line number, violated directive, user agent, etc.
- Combining CSP reporting with a security information and event management (SIEM) system helps you spot attempted XSS and adjust policies accordingly.
Example: Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘nonce-abc123’; report-uri https://example.com/csp-report; connect-src ‘self’; Or modern approach: Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘nonce-abc123’; report-to csp-endpoint Then define in the Reporting API: { “group”: “csp-endpoint”, “max_age”: 10886400, “endpoints”: [ { “url”: “https://example.com/csp-report” } ], “include_subdomains”: true }
Implementing CSP in real apps
- Start with a strict policy in production and gradually relax only what is necessary.
- Prefer serving CSP via HTTP headers rather than meta tags for stronger coverage and to prevent accidental policy overrides.
- For React, Vue, or other single-page apps that fetch scripts dynamically, rely on external script sources and avoid inline scripts where possible.
- If third-party scripts are essential, explicitly whitelist their domains and consider using Subresource Integrity (SRI) for script/style assets.
Example header for a modern app: Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘nonce-abc123’ https://cdn.example.com; style-src ‘self’ ‘nonce-abc123’ https://fonts.googleapis.com; img-src ‘self’ data:; connect-src ‘self’; font-src ‘self’ https://fonts.gstatic.com; object-src ‘none’; frame-ancestors ‘none’; form-action ‘self’; report-uri https://example.com/csp-report
HTML snippet to pair with the policy:
Testing CSP in development and staging
- Start with a permissive policy and gradually tighten it in a staging environment.
- Use browser developer tools to view CSP violations in the Console and the Violation Details.
- Employ automated tests that simulate common XSS vectors to ensure violations are reported and blocked.
- Validate that legitimate functionality (analytics, widgets, or third-party libraries) continues to work by validating that their sources are whitelisted.
Common pitfalls and how to avoid them
- Inline event handlers: Move logic from inline event handlers (onclick, onload) to external scripts or use nonces/hashes if inline is necessary.
- Third-party widgets: Ensure you whitelist trusted domains and consider sandboxing or using iframe-based integrations when possible.
- Overly permissive defaults: Avoid default-src * or script-src unsafe-inline. Prefer minimal, explicit policies.
- Over-restrictive policies breaking functionality: Incrementally tighten the policy and test to identify required sources.
Conclusion
A well-crafted Content Security Policy is a strong line of defense against XSS. By defaulting to a strict policy, using nonces or hashes for inline scripts, and enabling violation reporting, you gain visibility into attempted attacks and dramatically reduce the risk surface. CSP is most effective when integrated into a broader security program that includes input validation, output escaping, and secure development practices. Start with a solid baseline, test thoroughly, and iterate based on real-world telemetry from CSP reports.