Detection Engineering

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.

A dark browser developer console showing cyan code with one line flagged in red
Threat reference

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.

VariantHow it firesWhere it’s visibleCaught by
ReflectedPayload echoed back from the requestRequest parameters in web logsWeb logs + CSP report
StoredSaved once, runs for every viewerNothing at view time in request logsCSP violation report
DOM-basedClient JS writes input into a sinkNever touches server logsCSP 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.

SPL Likely XSS from CSP Violation Reports
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.

Sigma Web Request Containing Reflected XSS Tokens
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:

  1. Report-Only first. Deploy Content-Security-Policy-Report-Only so violations are logged but nothing is blocked. Your site keeps working.
  2. Triage the reports. Separate legitimate first-party scripts (fix the policy/nonce) from third-party noise (filter it).
  3. Drive violations to near-zero, then switch to the enforcing Content-Security-Policy header.
  4. 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 — HttpOnly keeps document.cookie from leaking the session.
  • Validate input at the boundary as defense in depth, not the only control.

Common CSP mistakes

  • Leaving unsafe-inline in 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:

  1. Replace any allowlist CSP with script-src 'nonce-{RANDOM}' 'strict-dynamic'.
  2. Remove unsafe-inline and unsafe-eval entirely.
  3. Generate a fresh, cryptographically strong nonce per response; stamp it on every legit script.
  4. Add report-uri/report-to and pipe violation reports to the SIEM.
  5. Deploy in Content-Security-Policy-Report-Only, triage violations, then enforce.
  6. Filter extension/analytics noise (chrome-extension://, known hosts) from the report feed.
  7. Encode output by context; gate dangerouslySetInnerHTML, v-html, and raw innerHTML in review.
  8. Sanitize user-supplied HTML with DOMPurify — never a regex denylist.
  9. Set HttpOnly and SameSite on session cookies.
  10. Alert on new blocked-uri hosts 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 Training
    Start 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.

Newsletter

Liked this breakdown?

Defensive security research — detection, hardening, and hardware — delivered when there is something worth saying. No spam, unsubscribe anytime.