XSS CSP Hardening for Blue Teams
XSS CSP hardening for blue teams — a strict nonce-based policy, CSP violation reports as a detection feed, Sigma and Suricata rules, tuning, and MITRE mapping.
XSS CSP hardening means replacing allowlist Content-Security-Policy rules with a strict, nonce-based policy and then wiring CSP violation reports into your SIEM as a live detection signal. A strict CSP neutralizes most reflected, stored, and DOM-based cross-site scripting even when the underlying code is still vulnerable. The reports it emits are the cheapest XSS intrusion-detection feed you will ever deploy. This guide ships both halves — the policy and the detection — plus how to roll it out without breaking the site.
Cross-site scripting still lives inside OWASP’s A03:2021 — Injection category, and it remains the most common way an attacker runs JavaScript in your users’ browsers. Output encoding alone is fragile; a strict CSP plus reporting is what blue teams should deploy.
What is XSS CSP hardening?
XSS CSP hardening is the practice of configuring a Content Security Policy strict enough to neutralize cross-site scripting, then using the policy’s own violation reports as a detection signal. It does two jobs at once: the browser refuses to run unauthorized script (prevention), and it tells you every time it had to (detection). That dual role is why it belongs in every blue team’s playbook.
A successful XSS payload runs with your application’s origin and your user’s session. That means session theft, credential harvesting via fake forms, and silent actions taken as the victim. It maps to MITRE ATT&CK T1059.007 — JavaScript for execution and T1185 — Browser Session Hijacking for impact.
What are the three types of XSS?
XSS comes in three shapes, and your policy and detection have to account for all three. The table maps each to where it is visible — which is the whole argument for CSP reporting.
| Variant | How it fires | Where it’s visible | Caught by |
|---|---|---|---|
| Reflected | Payload echoed back from the request | Request parameters in web logs | Web logs + CSP report |
| Stored | Saved once, runs for every viewer | Nothing at view time in request logs | CSP violation report |
| DOM-based | Client JS writes input into a sink | Never touches server logs | CSP violation report |
Request logs only see the loud reflected variant. Stored and DOM-based XSS are visible only at the browser, at render time — which is exactly what CSP violation reporting instruments. If your detection is request-log only, two of the three families are invisible.
How to build a strict, nonce-based CSP
Allowlist CSPs (script-src 'self' https://cdn.example.com ...) are
notoriously hard to get right
and frequently bypassable. The current OWASP and Google recommendation is a
strict policy built on per-response nonces, with reporting attached:
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none'; base-uri 'none';
report-uri /csp-report; report-to csp-endpoint
Three rules make or break it: every response gets a fresh, cryptographically
strong, base64 nonce stamped on every legitimate script; never ship
unsafe-inline or unsafe-eval; and use strict-dynamic so nonce’d scripts can
load framework code. An injected script has no valid nonce, so the browser refuses
to run it — even when your code emitted it.
How to detect XSS with CSP violation reports
The policy and the sensor are the same mechanism. With report-to/report-uri
set, the browser POSTs a JSON report every time it blocks a script. Forwarded to
your SIEM, that becomes a real-time XSS feed — including the stored and DOM
variants your logs never see.
index=csp sourcetype=csp:report
| spath "csp-report.blocked-uri" output=blocked
| spath "csp-report.violated-directive" output=directive
| where directive="script-src" AND blocked!="inline"
| stats count by page, blocked, src_ip
| where count >= 3 For the loud reflected variant, a request-log Sigma rule adds early warning at the edge — the same web-tier approach used for SQL injection detection.
title: Web Request Containing Reflected XSS Tokens
id: 9a1c7e44-darkpwn-illustrative
status: experimental
logsource:
category: webserver
detection:
selection:
cs-uri-query|contains:
- '<script'
- 'onerror='
- 'onload='
- 'javascript:'
- 'document.cookie'
condition: selection
falsepositives:
- WYSIWYG editors and scanners that legitimately pass markup
level: medium How to roll out CSP without breaking the site
The fastest way to kill a CSP project is to enforce a broken policy on day one. Stage it:
- Report-Only first. Deploy
Content-Security-Policy-Report-Onlyso violations are logged but nothing is blocked. Your site keeps working. - Triage the reports. Separate legitimate first-party scripts (fix the policy/nonce) from third-party noise (filter it).
- Drive violations to near-zero, then switch to the enforcing
Content-Security-Policyheader. - Keep the report endpoint live after enforcing. It is now your XSS IDS, not just a rollout tool.
Do not leave the policy in Report-Only forever — reports without enforcement give you detection but zero blocking.
How to defend against XSS beyond CSP
CSP is the safety net; the bug class still needs closing at the source.
- Set HttpOnly and SameSite on session cookies to blunt the payoff —
HttpOnlykeepsdocument.cookiefrom leaking the session. - Validate input at the boundary as defense in depth, not the only control.
Common CSP mistakes
- Leaving
unsafe-inlinein to “make it work.” It re-opens the exact hole the policy closes. - Stuck in Report-Only forever. Detection without blocking is half a control.
- Muting the report feed because of extension noise — filter, don’t mute.
- Hash-based policies on changing scripts. A one-byte edit breaks the hash; prefer nonces for dynamic apps.
XSS CSP hardening checklist
A copy-paste rollout list for your blue team:
- Replace any allowlist CSP with
script-src 'nonce-{RANDOM}' 'strict-dynamic'. - Remove
unsafe-inlineandunsafe-evalentirely. - Generate a fresh, cryptographically strong nonce per response; stamp it on every legit script.
- Add
report-uri/report-toand pipe violation reports to the SIEM. - Deploy in
Content-Security-Policy-Report-Only, triage violations, then enforce. - Filter extension/analytics noise (
chrome-extension://, known hosts) from the report feed. - Encode output by context; gate
dangerouslySetInnerHTML,v-html, and rawinnerHTMLin review. - Sanitize user-supplied HTML with DOMPurify — never a regex denylist.
- Set
HttpOnlyandSameSiteon session cookies. - Alert on new
blocked-urihosts appearing right after a deploy.
The takeaway
You cannot guarantee the absence of an XSS bug, so you deploy a control that refuses to run injected script and reports every attempt. The strict policy is the shield; the violation reports are the sensor. Ship both — and continue the web-application defense arc with SSRF detection and the wider Detection Engineering pillar.
Training & tools referenced
Disclosure: Some links below are affiliate links. If you buy through them, darkpwn may earn a commission at no extra cost to you. We only recommend training and tools we actually use in our own lab, and affiliate links never influence editorial coverage.
- TryHackMeAuthorized labs to practice XSS detection and CSP hardeningSecurity TrainingStart training
Frequently asked questions
Does a Content Security Policy stop all XSS?
No, but a strict nonce-based CSP neutralizes most reflected, stored, and DOM-based XSS even when the underlying code is vulnerable. It is defense in depth on top of context-aware output encoding, not a replacement for it.
What is the difference between a nonce and a hash in CSP?
A nonce is a fresh random value generated per response and stamped on every legitimate script tag. A hash pins the exact contents of an inline script. Nonces suit dynamic apps; hashes break if the script changes by even one byte, including whitespace.
Should CSP run in report-only or enforce mode?
Start in Content-Security-Policy-Report-Only to collect violations without breaking the site, fix the legitimate ones, then switch to the enforcing header. Do not leave it in report-only forever — reports without enforcement give you detection but zero blocking.
Why is strict-dynamic recommended over an allowlist CSP?
Allowlist policies are hard to get right and bypassable through hosted endpoints or open redirects on allowlisted domains. A strict-dynamic policy trusts scripts by nonce and lets them load further scripts, which is what makes modern frameworks work under a strict policy.