Web Security Headers: A Practical CSP Implementation Guide

Retool's new app builder is where AI-generated code ships safely
Building apps with AI is easy. Getting them to production safely is another story.
HTTP security headers are the cheapest high-value control in web security. They are server-side response headers, broadly supported across modern browsers, require no application changes for most of them, and mitigate entire classes of attack: protocol downgrade (HSTS), clickjacking (frame-ancestors), MIME confusion (X-Content-Type-Options), cross-site scripting (CSP), and referrer leakage (Referrer-Policy). They are also consistently misconfigured, with the most common failure being CSP policies that disable themselves by including 'unsafe-inline' to avoid breaking the site, which is roughly equivalent to leaving the front door open because the lock is inconvenient.
This guide covers the six headers that deliver most of the defensive value, the deployment strategy that lets you ship a strict CSP without breaking your application, and the gotchas that come up in real environments (CDNs that inject scripts, analytics platforms that change their domains, HSTS preload commitments that cannot be undone). The objective is a security headers configuration that grades A or A+ on securityheaders.com and Mozilla Observatory without disabling the protections that drove the grade.
The Headers That Actually Deliver Value
Strict-Transport-Security (HSTS) forces browsers to use HTTPS for your domain and prevents protocol downgrade attacks. Set Strict-Transport-Security: max-age=31536000; includeSubDomains; preload once you are confident every endpoint on every subdomain serves HTTPS. Content-Security-Policy (CSP) controls which sources the browser is allowed to load resources from and which inline code is allowed to execute; it is the only header that meaningfully mitigates XSS once it has occurred. X-Content-Type-Options: nosniff disables MIME type sniffing, preventing the browser from interpreting a text response as JavaScript or HTML based on content inspection. Referrer-Policy controls how much of the current URL is sent in the Referer header on outbound requests; strict-origin-when-cross-origin is a sensible default that sends only the origin on cross-origin requests. Permissions-Policy (formerly Feature-Policy) controls which browser features (camera, microphone, geolocation, payment, USB) the page can use, defaulting to deny everything not explicitly needed. X-Frame-Options is the legacy header for clickjacking protection; it is superseded by CSP frame-ancestors which is more expressive (supports multiple sources, supports paths). Other headers (X-XSS-Protection, X-Permitted-Cross-Domain-Policies, Expect-CT, Cross-Origin-Resource-Policy, Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy) range from deprecated (X-XSS-Protection) to situationally valuable (the cross-origin isolation headers if you need SharedArrayBuffer). Focus on the six above first.
HSTS Deployment and the Preload Commitment
HSTS deployment has three phases. First, set Strict-Transport-Security: max-age=300 to begin enforcement with a short TTL while you verify nothing breaks. Second, raise the max-age to 31536000 (one year) and add includeSubDomains. Third, add preload and submit to the HSTS preload list at hstspreload.org. Preload bakes your domain into the browser binary so that the very first request to your domain is forced to HTTPS, eliminating the trust-on-first-use window where HSTS without preload is bypassable by an attacker controlling the initial DNS response. The preload list is shipped with Chrome, Firefox, Safari, and Edge. The commitment is significant: includeSubDomains means every current and future subdomain must serve valid HTTPS, including subdomains you do not currently use. If you have a marketing team that occasionally spins up campaign subdomains on HTTP, preload will break them and removal from the preload list takes weeks at minimum. Audit every subdomain (including those behind firewalls, on internal DNS) before submitting. The maximum supported max-age for preload is unbounded; the minimum to qualify for the list is 31536000 (one year). Once preloaded, treat HSTS as a one-way commitment.
Briefings like this, every morning before 9am.
Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.
Content Security Policy: The Directive Hierarchy
CSP works by allowlisting sources for each resource type. The directive hierarchy: default-src is the fallback for any fetch directive not explicitly set. script-src controls JavaScript sources, including inline scripts; this is the most security-critical directive. style-src controls CSS sources. img-src controls image sources. connect-src controls XHR, fetch, WebSocket, and EventSource targets. font-src controls web font sources. frame-src controls iframe sources. frame-ancestors controls who can frame this page (clickjacking protection, supersedes X-Frame-Options). form-action controls form submission targets. base-uri restricts the <base> element to prevent attackers from rewriting relative URL resolution. object-src 'none' disables Flash, Java applets, and PDF embeds. report-uri (legacy) or report-to (modern) specifies an endpoint to receive violation reports. A reasonable starting policy: default-src 'self'; script-src 'self' 'nonce-{random}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; object-src 'none'; report-uri /csp-report. The 'nonce-{random}' value must be a fresh cryptographically random value generated per response and included in the nonce attribute of every legitimate inline script: <script nonce='abc123'>. Inline scripts without the matching nonce are blocked; injected XSS payloads cannot guess the nonce.
Rollout Strategy: Report-Only First, Enforce Second
Deploying a strict CSP straight into enforcement will break your site. Every legitimate inline script, every third-party widget, every analytics snippet will be blocked, and your users will see broken pages. The disciplined rollout is two-phase. Phase one: deploy with Content-Security-Policy-Report-Only header instead of Content-Security-Policy. Report-Only does not block anything; it only sends violation reports to your report-uri endpoint. Set up a violation collector (Sentry, Datadog, or a simple JSON endpoint that logs to your SIEM) and collect violations for at least two weeks across all user populations, including mobile, internationalized users, and users with browser extensions. Analyze the violations: distinguish legitimate sources you forgot (your CDN, your analytics provider, your error tracker) from extension noise (chrome-extension://, moz-extension://, safari-extension://) which you can usually filter out from reports. Add legitimate sources to your policy. Iterate until the violation rate drops to a manageable level dominated by extension noise. Phase two: switch the header name from Content-Security-Policy-Report-Only to Content-Security-Policy and keep monitoring. Keep the report-uri active in enforcement mode so you catch any new sources introduced by feature work. For new applications, you can skip Report-Only by building with CSP discipline from day one, but for any existing application with more than trivial JavaScript, Report-Only first is the only safe path.
Avoiding unsafe-inline and Common Breakage Scenarios
The default impulse when CSP breaks something is to add 'unsafe-inline' to script-src, which silently disables most of the XSS protection CSP provides. The disciplined alternative is nonces or hashes. Nonces: generate a fresh random value per response (at least 128 bits of entropy, base64-encoded), include it in the CSP header as 'nonce-{value}' in script-src, and emit it on every legitimate inline script tag. The application framework (Next.js, Rails, Django) typically has middleware or template helpers for this. Hashes: for static inline scripts whose content does not change, compute the SHA-256 hash of the script content and include 'sha256-{base64hash}' in script-src; the browser allows only inline scripts whose content matches the hash. Hashes work for build-time constants but not for dynamic content. For 'unsafe-eval', which permits eval, new Function, and similar dynamic code generation, the answer is to remove eval from your code; modern frameworks rarely require it, and the libraries that do (some templating engines, some JSON parsers) usually have safe alternatives. Common breakage scenarios: CDNs like Cloudflare Rocket Loader inject inline scripts that need either disabling at the CDN level or whitelisting via hash, analytics platforms (Google Tag Manager, Segment) sometimes load secondary scripts from domains not in your initial allowlist, third-party widgets (chat, support, comment systems) often require 'unsafe-inline' or specific domains; isolate these in dedicated iframes with their own permissive CSP rather than weakening your main page policy. Grade your final configuration on securityheaders.com and observatory.mozilla.org, but treat the grade as a sanity check rather than a goal; an A+ grade with 'unsafe-inline' is worse than a B grade with a strict nonce-based policy.
The bottom line
Security headers are free, broadly supported, and disproportionately effective. Six headers (HSTS, CSP, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and frame-ancestors via CSP) cover the major attack classes that headers can mitigate, and deploying them correctly is a matter of process rather than complexity.
The critical decisions are CSP rollout discipline (Report-Only first, then enforce, never 'unsafe-inline' as a quick fix) and HSTS preload commitment (audit subdomains, then commit). Get those right and the rest is configuration.
Frequently asked questions
Should I use X-Frame-Options or CSP frame-ancestors?
Use CSP frame-ancestors only for new deployments. It is more expressive (supports multiple sources and paths), is the modern standard, and supersedes X-Frame-Options in browsers that support both. Setting both is harmless and supports older browsers, but if you set only one, choose frame-ancestors.
Is HSTS preload reversible?
Removal from the preload list is possible but takes weeks to months as the change propagates through browser releases. Treat preload as a one-way commitment and audit every subdomain (including unused and internal ones) before submitting, because includeSubDomains will force HTTPS on all of them.
Why is 'unsafe-inline' in script-src bad?
It permits any inline script to execute, including scripts injected via XSS, which defeats most of CSP's protection against XSS. Use nonces (per-response random values) or hashes (for static inline content) instead. 'unsafe-inline' is acceptable in style-src as a transitional measure but should be removed from script-src.
How do I roll out CSP without breaking my site?
Deploy in Content-Security-Policy-Report-Only mode first, collect violation reports for at least two weeks across all user populations, identify legitimate sources you missed, iteratively refine the policy, and only then switch to enforcing Content-Security-Policy. Skipping the Report-Only phase on an existing application will break legitimate functionality.
What is a sensible Permissions-Policy default?
Deny everything not explicitly used: Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(). Add specific features back as needed by your application. This prevents third-party scripts and iframes from quietly accessing sensitive browser features.
Sources & references
Free resources
Critical CVE Reference Card 2025–2026
25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.
Ransomware Incident Response Playbook
Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.
Get threat intel before your inbox does.
50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.
Unsubscribe anytime. We never sell your data.

Founder & Cybersecurity Evangelist, Decryption Digest
Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.
Win a $2,495 Black Hat pass.
Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.
