← News
/PLAYBOOK · 2026-08-29

CSP Configuration in Practice: XSS Defense-in-Depth Guide

Content Security Policy (CSP) allowlists which origins the browser may load, blocking XSS, data injection, and clickjacking. This article covers core CSP directives, Nginx response-header config, nonce allowlisting for inline scripts, staged rollout, and false-positive triage.

Applicable scenarios

CSP configuration applies when a site has XSS or frontend injection risk and needs a browser-side allowlist as defense in depth; when pages pull third-party CDN scripts, analytics, or other external resources and load origins must be tightly controlled; or when PCI-DSS / MLPS 2.0 checks require Web application security response headers. CSP is a browser-native control. Even if app-layer filters are bypassed, the browser still blocks malicious script execution. Highest ROI layer in an XSS defense stack.

Prerequisites

How it works

Content Security Policy tells the browser, via the Content-Security-Policy response header, which origins may load resources. Core directives: default-src (fallback), script-src (scripts), style-src (styles), img-src (images), connect-src (XHR/fetch), frame-ancestors (embed origins; clickjacking defense). Loads outside the allowlist are blocked; the console logs a violation. Inline script and eval() are denied by default. To allow them, authorize precisely with a nonce (one-time random token) or a hash (script-content hash). Do not use 'unsafe-inline'—it guts the control.

Procedure

1. Inventory page resources

Use the browser DevTools Network panel. Collect every request, group domains by type, use that as the allowlist:

# Fetch the page and extract external resource domains (illustrative)
curl -s https://example.com | grep -oE 'src="[^"]*' | cut -d'/' -f3 | sort -u

2. Nginx baseline policy

# Add the response header in the server block
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;

Baseline: same-origin only. Safest starting point. Save and reload:

nginx -t && systemctl reload nginx

3. Allow external resources as needed

Append domain allowlists from the inventory, for example:

add_header Content-Security-Policy "default-src 'self';
    script-src 'self' https://cdn.example.com https://www.googletagmanager.com;
    style-src 'self' https://cdn.example.com;
    img-src 'self' data: https://img.example.com;
    connect-src 'self' https://helixcyber.ai;" always;

4. Allow inline scripts with a nonce

Critical inline scripts can be allowlisted precisely with a nonce:

# Nginx generates a random nonce and injects it into the response header (SSI or Lua module example)
set $nonce $request_id;
add_header Content-Security-Policy "script-src 'self' 'nonce-$nonce';" always;

# Inline script tag carries the same nonce attribute
<script nonce="{{nonce}}">console.log('ok');</script>

5. Staged rollout: report-only first

Use Content-Security-Policy-Report-Only to report without blocking. Watch live pages for gaps:

add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report;" always;
# Nginx location to receive violation reports (illustrative)
location = /csp-report {
    proxy_pass http://127.0.0.1:9000/csp;   # forward to log collection
    access_log /var/log/nginx/csp.log;
}

6. Enforce in production and keep watching

# After report-only shows no mass false positives, switch to enforce
add_header Content-Security-Policy "...complete policy..." always;

# Regularly mine violation logs; update the allowlist when new resources appear
grep -oE 'blocked-uri[^,]*' /var/log/nginx/csp.log | sort | uniq -c | sort -rn | head

Verification

# 1. Confirm the response header is present
curl -sI https://example.com | grep -i content-security-policy

# 2. Regression: login, payment, analytics all work
# 3. Browser console has no CSP violations (F12 → Console)
# 4. Score the policy with a scanner (CSP Evaluator / Google online checker)

Pass criteria: Content-Security-Policy is on the response; policy does not contain 'unsafe-inline' or 'unsafe-eval' (except documented exceptions); console has no Refused to load errors; core business paths work.

FAQ

Q1: After CSP went live, third-party analytics/ad scripts were blocked and metrics disappeared. What now?

Two cases. Fixed-domain script: append that domain to script-src. Inline-injected or dynamically loaded script: switch to nonce; backend emits a random token on every legitimate script tag. Do not add 'unsafe-inline'—that makes CSP nearly useless against XSS. Run Report-Only for a week first. Inventory every blocked resource, then freeze the policy.

Q2: The page has a lot of inline styles and eval. How do we migrate without breakage?

Inline styles: extract to external files over time, or keep 'unsafe-inline' on style-src only (lower risk than script; still acceptable). eval-class code (old webpack, Vue 2 dev mode): upgrade the framework first. Temporary: allow 'unsafe-eval' with a deadline. Principle: strict on script, looser on style, tighten on a schedule. Do not flip to full block in one shot and take production down.

Summary

CSP is the browser's last line of defense. Low cost, blocks most XSS and data-injection attacks. Landing path: inventory resources → baseline allowlist → report-only stage → enforce → continuous monitor. Nonce for necessary inline scripts balances security and availability. Put CSP checks in CI. New features must prove the header is intact so the policy is not deleted or weakened.