Implementing Strict Content Security Policy (CSP) & Security Headers
Comprehensive technical guide for configuring strict Content Security Policy (CSP), HTTP Strict Transport Security (HSTS), X-Content-Type-Options, and Nginx security headers.
Executive Summary & Key Takeaways
- XSS Mitigation: Content Security Policy (CSP) stops Cross-Site Scripting by restricting script execution to cryptographically nonced or hashed scripts.
- Clickjacking Defense: X-Frame-Options: DENY and frame-ancestors 'none' prevent malicious iframe embedding.
- Strict Transport Security: Enforce HTTPS and HSTS preloading with max-age=63072000; includeSubDomains; preload.
- MIME Sniffing Defense: X-Content-Type-Options: nosniff prevents browsers from misinterpreting non-executable files as executable scripts.
Cross-Site Scripting (XSS) remains one of the most pervasive vulnerabilities in modern web applications. Even with rigorous input sanitization and output encoding, application bugs can allow attackers to inject malicious JavaScript, steal session cookies, or intercept sensitive user data.
A Strict Content Security Policy (CSP) serves as a critical secondary layer of defense, instructing the browser to reject unauthorized scripts, inline execution, and unauthorized network connections even if an XSS injection occurs.
1. Understanding Content Security Policy (CSP)
CSP is an HTTP response header that defines an explicit allowlist of trusted resource origins. Without a CSP, browsers execute any JavaScript present in the HTML document, including malicious payloads injected via DOM manipulation or reflected parameters. With a strict CSP, the browser enforces execution rules before running script blocks or executing dynamic fetches.
2. Defense-in-Depth: Why Input Sanitization Alone Is Insufficient
While input sanitization (using tools like DOMPurify) and output encoding are essential first lines of defense, complex modern web applications with client-side templating frameworks frequently suffer from edge-case vulnerabilities:
- DOM-based XSS: Unsafe handling of location.hash or document.referrer in client-side scripts.
- Dependency Vulnerabilities: Malicious or compromised third-party npm packages introducing backdoor execution scripts.
- Dangling Markup Injections: Exploiting unclosed HTML attributes to exfiltrate CSRF tokens or sensitive form data.
A strict Content Security Policy acts as an independent safety net: even if an attacker successfully injects a payload into the DOM, the browser refuses to fetch or execute the file because evil.com is not explicitly nonced or trusted.</p> <h2> <a name="3-strict-csp-directives-noncebased-amp-hashbased-execution" href="#3-strict-csp-directives-noncebased-amp-hashbased-execution" class="anchor"> </a> 3. Strict CSP Directives: Nonce-Based & Hash-Based Execution </h2> <p>Legacy CSP configurations relied on domain allowlists (e.g., script-src 'self' <a href="https://trustedcdn.com">https://trustedcdn.com</a>). However, security research demonstrates that domain allowlists can often be bypassed via JSONP endpoints or open redirects on whitelisted domains.</p> <p>Modern security standards mandate a Strict Nonce-Based CSP or Strict Hash-Based CSP:<br> </p> <div class="highlight"><pre class="highlight http"><code><span class="err"># Recommended Strict Nonce-Based Content Security Policy Header Content-Security-Policy: default-src 'none'; script-src 'nonce-rAnd0mN0nc3vAlu3' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; upgrade-insecure-requests; </span></code></pre></div> <p></p> <p>Key directives breakdown:</p> <ul> <li>default-src 'none': Fallback rule denying all resource loading unless explicitly allowed.</li> <li>nonce-'<random>': Requires a unique 128-bit cryptographically secure random token per request for inline scripts.</li> <li>'strict-dynamic': Enables trusted scripts (with a valid nonce) to dynamically load downstream scripts without explicit domain allowlisting.</li> <li>frame-ancestors 'none': Prevents the page from being embedded in <iframe> tags, defending against Clickjacking.</li> <li>base-uri 'none': Prevents attackers from injecting <base> tags to hijack relative URLs.</li> </ul> <h2> <a name="3-configuring-nginx-amp-apache-security-headers" href="#3-configuring-nginx-amp-apache-security-headers" class="anchor"> </a> 3. Configuring Nginx & Apache Security Headers </h2> <p>To achieve maximum HTTP security ratings (A+ rating on SSL Labs and SecurityHeaders.com), configure Nginx to deliver a comprehensive suite of security headers:<br> </p> <div class="highlight"><pre class="highlight nginx"><code><span class="c1"># /etc/nginx/conf.d/security-headers.conf</span> <span class="c1"># Content Security Policy (Strict Nonce/Self Baseline)</span> <span class="k">add_header</span> <span class="s">Content-Security-Policy</span> <span class="s">"default-src</span> <span class="s">'none'</span><span class="p">;</span> <span class="k">script-src</span> <span class="s">'self'</span><span class="p">;</span> <span class="k">style-src</span> <span class="s">'self'</span> <span class="s">'unsafe-inline'</span><span class="p">;</span> <span class="k">img-src</span> <span class="s">'self'</span> <span class="s">data:</span><span class="p">;</span> <span class="k">font-src</span> <span class="s">'self'</span><span class="p">;</span> <span class="k">connect-src</span> <span class="s">'self'</span><span class="p">;</span> <span class="k">frame-ancestors</span> <span class="s">'none'</span><span class="p">;</span> <span class="k">base-uri</span> <span class="s">'none'</span><span class="p">;</span> <span class="k">form-action</span> <span class="s">'self'</span><span class="p">;</span> <span class="k">upgrade-insecure-requests</span><span class="p">;</span><span class="k">"</span> <span class="s">always</span><span class="p">;</span> <span class="c1"># HTTP Strict Transport Security (HSTS 2 Years + Preload)</span> <span class="k">add_header</span> <span class="s">Strict-Transport-Security</span> <span class="s">"max-age=63072000</span><span class="p">;</span> <span class="k">includeSubDomains</span><span class="p">;</span> <span class="k">preload"</span> <span class="s">always</span><span class="p">;</span> <span class="c1"># Prevent MIME-Sniffing Exploits</span> <span class="k">add_header</span> <span class="s">X-Content-Type-Options</span> <span class="s">"nosniff"</span> <span class="s">always</span><span class="p">;</span> <span class="c1"># Frame Options (Anti-Clickjacking)</span> <span class="k">add_header</span> <span class="s">X-Frame-Options</span> <span class="s">"DENY"</span> <span class="s">always</span><span class="p">;</span> <span class="c1"># Referrer Policy (Protect User Privacy)</span> <span class="k">add_header</span> <span class="s">Referrer-Policy</span> <span class="s">"strict-origin-when-cross-origin"</span> <span class="s">always</span><span class="p">;</span> <span class="c1"># Permissions Policy (Disable Unused Web APIs)</span> <span class="k">add_header</span> <span class="s">Permissions-Policy</span> <span class="s">"geolocation=(),</span> <span class="s">microphone=(),</span> <span class="s">camera=(),</span> <span class="s">payment=(),</span> <span class="s">usb=()"</span> <span class="s">always</span><span class="p">;</span> </code></pre></div> <p></p> <h2> <a name="4-trusted-types-amp-subresource-integrity-sri" href="#4-trusted-types-amp-subresource-integrity-sri" class="anchor"> </a> 4. Trusted Types & Subresource Integrity (SRI) </h2> <p>Modern web applications handling user-generated input can enforce Trusted Types via the require-trusted-types-for 'script' directive. Trusted Types prevents DOM-based XSS by requiring string inputs passed to dangerous sink functions (such as innerHTML, document.write, or eval) to be wrapped in a sanitizing TrustedHTML policy object.<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="c1">// Enforcing Trusted Types API in Client-Side Scripts</span> <span class="k">if </span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">trustedTypes</span> <span class="o">&&</span> <span class="nx">trustedTypes</span><span class="p">.</span><span class="nx">createPolicy</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">escapeHTMLPolicy</span> <span class="o">=</span> <span class="nx">trustedTypes</span><span class="p">.</span><span class="nf">createPolicy</span><span class="p">(</span><span class="dl">'</span><span class="s1">myEscapePolicy</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">createHTML</span><span class="p">:</span> <span class="nx">string</span> <span class="o">=></span> <span class="nx">DOMPurify</span><span class="p">.</span><span class="nf">sanitize</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span> <span class="p">});</span> <span class="c1">// Safe DOM assignment via TrustedHTML policy</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">output</span><span class="dl">'</span><span class="p">).</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">escapeHTMLPolicy</span><span class="p">.</span><span class="nf">createHTML</span><span class="p">(</span><span class="nx">userInput</span><span class="p">);</span> <span class="p">}</span> </code></pre></div> <p></p> <p>Combine Trusted Types with Subresource Integrity (SRI) hashes on external CDN scripts to guarantee that third-party library files have not been modified or compromised in transit:<br> </p> <div class="highlight"><pre class="highlight html"><code><span class="c"><!-- Subresource Integrity (SRI) Cryptographic Hash Verification --></span> <span class="nt"><script </span><span class="na">src=</span><span class="s">"https://cdn.example.com/library.js"</span><span class="nt">></script></span> </code></pre></div> <p></p> <h2> <a name="5-automated-csp-violation-reporting-reportto" href="#5-automated-csp-violation-reporting-reportto" class="anchor"> </a> 5. Automated CSP Violation Reporting (Report-To) </h2> <p>Deploying CSP in Content-Security-Policy-Report-Only mode allows you to monitor potential policy breaches without blocking legitimate scripts during testing:<br> </p> <div class="highlight"><pre class="highlight http"><code><span class="err">Content-Security-Policy-Report-Only: default-src 'none'; script-src 'self'; report-uri /api/csp-report; </span></code></pre></div> <p></p> <h2> <a name="5-security-headers-comparison-matrix" href="#5-security-headers-comparison-matrix" class="anchor"> </a> 5. Security Headers Comparison Matrix </h2> <p>Summary of mandatory HTTP security headers and their defense focus:</p> <h2> <a name="6-frequently-asked-questions-faq" href="#6-frequently-asked-questions-faq" class="anchor"> </a> 6. Frequently Asked Questions (FAQ) </h2> <p><strong>Q: What is Content Security Policy (CSP) and how does it prevent XSS?</strong></p> <p>Content Security Policy (CSP) is an HTTP header that allows site operators to restrict the resources (such as JavaScript, CSS, Images) that the browser is allowed to load for a given page, blocking injected Cross-Site Scripting (XSS) scripts.</p> <p><strong>Q: What is the difference between CSP Nonce and CSP Hash?</strong></p> <p>A CSP Nonce is a cryptographically strong, single-use random string generated on every request for inline scripts. A CSP Hash is a cryptographic SHA-256 hash of a static inline script content.</p> <hr> <p><em>Originally published at <a href="https://zyekh.com/blog/securing-web-applications-with-strict-content-security-policy.html">https://zyekh.com/blog/securing-web-applications-with-strict-content-security-policy.html</a></em></p>
Top comments (0)