Web Development

Web Security Headers: A Complete Guide to CSP, CORS, HSTS, and Beyond

Web Security Headers: A Complete Guide to CSP, CORS, HSTS, and Beyond

Why HTTP Security Headers Matter More Than You Think

Every HTTP response your server sends carries headers that instruct browsers how to handle your content. Most developers focus on functional headers — Content-Type, Cache-Control, Set-Cookie — but a parallel set of security headers exists that forms your application’s first line of defense against cross-site scripting, clickjacking, data injection, and protocol downgrade attacks.

Browsers are permissive by default. Without explicit security headers, a browser will load scripts from any domain, allow your page to be embedded in a malicious iframe, or submit cookies over unencrypted connections. Security headers flip this model — they tell browsers to restrict behavior unless explicitly permitted, turning the browser itself into a security enforcement layer.

This guide covers the essential HTTP security headers every web developer should implement: Content Security Policy (CSP), Cross-Origin Resource Sharing (CORS), HTTP Strict Transport Security (HSTS), and several others that collectively harden your application against common web attacks. Each header includes practical configuration examples you can deploy immediately.

Content Security Policy (CSP): Controlling What Your Page Can Load

Content Security Policy is the most powerful — and most complex — security header available. CSP lets you define exactly which sources of content are permitted on your page: scripts, stylesheets, images, fonts, frames, and more. Any resource that violates the policy gets blocked, and the browser reports the violation.

Without CSP, a single XSS vulnerability lets an attacker inject arbitrary scripts that execute with full access to your page’s DOM, cookies, and user data. CSP mitigates this by ensuring that even if an attacker injects a <script> tag, the browser refuses to execute it because the script’s source isn’t whitelisted.

CSP Directives You Need to Know

CSP works through directives — each directive controls a specific resource type:

  • default-src — fallback policy for any resource type not explicitly covered by another directive
  • script-src — controls JavaScript sources, the most critical directive for XSS prevention
  • style-src — controls CSS sources, including inline styles
  • img-src — controls image sources
  • connect-src — controls targets for fetch, XMLHttpRequest, WebSocket, and EventSource
  • font-src — controls web font sources
  • frame-src — controls sources that can be embedded in iframes on your page
  • frame-ancestors — controls which pages can embed your page in an iframe (replaces X-Frame-Options)
  • base-uri — restricts URLs that can appear in the <base> element
  • form-action — restricts URLs that forms can submit to

The keyword 'self' refers to the current origin. The keyword 'none' blocks all sources for that directive. Avoid 'unsafe-inline' and 'unsafe-eval' unless absolutely necessary — they negate most of CSP’s XSS protection.

Implementing CSP in Practice

Start with a report-only policy to discover what your page actually loads before enforcing restrictions. The Content-Security-Policy-Report-Only header applies the policy without blocking anything — it just reports violations. This prevents you from accidentally breaking your own site:

# Nginx configuration — report-only CSP for discovery phase
add_header Content-Security-Policy-Report-Only "
    default-src 'self';
    script-src 'self' https://cdn.example.com;
    style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
    img-src 'self' data: https://images.example.com;
    font-src 'self' https://fonts.gstatic.com;
    connect-src 'self' https://api.example.com;
    frame-ancestors 'self';
    base-uri 'self';
    form-action 'self';
    report-uri /csp-violation-report;
" always;

Monitor violation reports for a week or two, adjust the policy to cover legitimate resources, then switch from Content-Security-Policy-Report-Only to Content-Security-Policy to start enforcing. This phased approach is essential — deploying an enforced CSP without testing will break third-party scripts, analytics, embedded widgets, or CDN-served assets.

For modern applications that require inline scripts (many JavaScript frameworks inject inline code), use nonce-based CSP instead of 'unsafe-inline'. Generate a unique nonce per request and include it in both the header and the script tags:

# In your server-side template (e.g., Node.js / Express)
const crypto = require('crypto');
const nonce = crypto.randomBytes(16).toString('base64');

# Set the header
Content-Security-Policy: script-src 'self' 'nonce-${nonce}';

# In your HTML
<script nonce="${nonce}">
    // This script executes because its nonce matches the header
    initializeApp();
</script>

Nonce-based CSP provides strong XSS protection while accommodating the realities of modern frontend development. If you’re managing multiple environment variables and configurations across staging and production, consider generating CSP nonces as part of your server middleware rather than hardcoding them.

Cross-Origin Resource Sharing (CORS): Managing Cross-Domain Requests

CORS is not a security mechanism you add — it’s a relaxation of the Same-Origin Policy (SOP) that browsers enforce by default. The SOP blocks web pages from making requests to a different origin (domain, protocol, or port). CORS provides a structured way for servers to declare which cross-origin requests they permit.

Misunderstanding CORS causes two problems. Developers who don’t understand it set Access-Control-Allow-Origin: * to make errors go away, opening their API to any website. Developers who understand it too narrowly block legitimate requests from their own frontend applications on different subdomains.

How CORS Works

For simple requests (GET, POST with standard content types), the browser sends the request and checks the Access-Control-Allow-Origin response header. For complex requests (PUT, DELETE, custom headers, JSON content types), the browser first sends a preflight OPTIONS request to verify the server permits the actual request. The key CORS response headers are:

  • Access-Control-Allow-Origin — specifies which origin(s) can access the resource. Use a specific origin, not *, when credentials are involved
  • Access-Control-Allow-Methods — lists permitted HTTP methods (GET, POST, PUT, DELETE)
  • Access-Control-Allow-Headers — lists permitted request headers beyond the default safe set
  • Access-Control-Allow-Credentials — whether the browser should include cookies and auth headers. Must be true if your API uses session cookies or bearer tokens in cross-origin calls
  • Access-Control-Max-Age — how long (in seconds) the browser can cache the preflight response, reducing repeated OPTIONS requests

A common mistake when building APIs is to forget about CORS entirely during development, then scramble to configure it when the frontend, deployed on a different domain, can’t reach the backend. Plan your CORS policy as part of your API design process from the beginning.

CORS Configuration Guidelines

For APIs that serve a known set of frontends, whitelist specific origins rather than using wildcards. Validate the Origin header against your whitelist and reflect the matching origin in the response. Never blindly reflect the incoming Origin header — reflecting it without validation is equivalent to Access-Control-Allow-Origin: * with credentials enabled. Cache preflight responses aggressively with Access-Control-Max-Age: 86400 (24 hours) to eliminate repeated OPTIONS request overhead.

HTTP Strict Transport Security (HSTS): Forcing HTTPS Everywhere

HSTS tells browsers to only connect to your site over HTTPS, even if the user types http:// or clicks an HTTP link. Without HSTS, the initial HTTP request is vulnerable to man-in-the-middle attacks — an attacker on the same network can intercept the unencrypted request and redirect the user to a phishing page before the HTTPS redirect occurs.

The header is simple:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

  • max-age — how long (in seconds) the browser should remember to only use HTTPS. 31536000 seconds equals one year
  • includeSubDomains — applies the policy to all subdomains. Essential if your application uses subdomains for APIs, CDN assets, or user-generated content
  • preload — signals your intent to be included in browser HSTS preload lists. Once preloaded, browsers enforce HTTPS for your domain without needing the initial visit

Before deploying HSTS, ensure that every page on your domain and all subdomains work correctly over HTTPS. HSTS is effectively irreversible for the duration of max-age — if you set a one-year max-age and then discover that a subdomain doesn’t support HTTPS, visitors won’t be able to access it until the policy expires. Start with a short max-age (like 300 seconds) and increase it gradually as you verify everything works.

If you’re serving applications through Nginx, HSTS is a single line in your server block. For containerized deployments managed with Docker, configure HSTS at the reverse proxy level rather than in each container.

X-Content-Type-Options: Stopping MIME Sniffing

Browsers have a legacy behavior called MIME sniffing — if the Content-Type header is missing or seems wrong, the browser guesses the content type by inspecting the response body. This is dangerous because an attacker can upload a file with a .jpg extension that actually contains JavaScript. If the browser sniffs the content and decides it’s a script, it will execute it.

X-Content-Type-Options: nosniff

This single directive tells browsers to trust the Content-Type header and never sniff. There is no reason not to deploy this header on every response. It has no compatibility issues and no configuration complexity.

X-Frame-Options and frame-ancestors: Preventing Clickjacking

Clickjacking attacks embed your page in an invisible iframe on a malicious site, then overlay deceptive UI elements that trick users into clicking buttons on your page without realizing it. A user thinks they’re clicking “Play Video” but they’re actually clicking “Delete Account” on your framed page.

X-Frame-Options was the original defense:

  • DENY — the page cannot be framed by any site, including your own
  • SAMEORIGIN — the page can only be framed by pages on the same origin

The modern replacement is CSP’s frame-ancestors directive, which offers more granular control — you can whitelist specific domains that are allowed to frame your content. Deploy both for backward compatibility with older browsers that don’t support CSP:

X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'

Referrer-Policy: Controlling Information Leakage

When a user clicks a link on your page, the browser sends a Referer header to the destination site containing the URL of your page. This can leak sensitive information — query parameters, internal paths, user IDs embedded in URLs, and other data you don’t want shared with third parties.

The Referrer-Policy header controls how much referrer information is sent. The recommended value for most applications is strict-origin-when-cross-origin — it sends the full URL for same-origin requests (preserving your analytics), only the origin domain for cross-origin HTTPS requests, and nothing for HTTPS-to-HTTP downgrades. Other options include no-referrer (never send referrer information) and same-origin (send referrer only for same-origin requests).

Permissions-Policy: Restricting Browser Features

The Permissions-Policy header (formerly Feature-Policy) controls which browser features your page can use: camera, microphone, geolocation, payment APIs, and more. Even if an attacker injects code, they cannot access blocked features.

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()

The empty parentheses () disable the feature entirely. You can also allow features for specific origins:

Permissions-Policy: geolocation=(self "https://maps.example.com")

Set this header restrictively by default and only enable features your application actually needs. This defense-in-depth approach limits the blast radius of any future XSS vulnerability.

Putting It All Together: A Production Security Headers Configuration

Here is a complete Nginx configuration block that implements all the security headers covered in this guide. This configuration represents a strong baseline — adjust the CSP policy to match your application’s specific resource requirements:

# Complete security headers — Nginx configuration
server {
    listen 443 ssl http2;
    server_name example.com;

    # HSTS — enforce HTTPS for one year, including subdomains
    add_header Strict-Transport-Security
        "max-age=31536000; includeSubDomains; preload" always;

    # CSP — restrict content sources (customize per your app)
    add_header Content-Security-Policy "
        default-src 'self';
        script-src 'self' https://cdn.example.com;
        style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
        img-src 'self' data: https://images.example.com;
        font-src 'self' https://fonts.gstatic.com;
        connect-src 'self' https://api.example.com wss://ws.example.com;
        frame-ancestors 'self';
        base-uri 'self';
        form-action 'self';
        upgrade-insecure-requests;
    " always;

    # Prevent MIME sniffing
    add_header X-Content-Type-Options "nosniff" always;

    # Clickjacking protection (CSP frame-ancestors above is primary)
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Referrer control
    add_header Referrer-Policy
        "strict-origin-when-cross-origin" always;

    # Restrict browser features
    add_header Permissions-Policy
        "camera=(), microphone=(), geolocation=(), payment=()" always;

    # Prevent cross-site scripting filter bypass in older browsers
    add_header X-XSS-Protection "0" always;

    # ... rest of your server configuration
}

Note the X-XSS-Protection: 0 value — the legacy XSS auditor has been deprecated because it introduced its own vulnerabilities, and modern CSP provides far superior protection. The upgrade-insecure-requests directive inside CSP tells browsers to automatically upgrade HTTP resource requests to HTTPS, which is useful during HTTP-to-HTTPS migrations.

Testing and Monitoring Your Security Headers

Deploying security headers without monitoring is flying blind. Third-party scripts update their domains, marketing adds new tracking pixels, developers embed resources from new CDNs. Without monitoring, these legitimate changes break silently.

Tools for Auditing Security Headers

Several free tools scan your site and grade your security header configuration:

  • securityheaders.com — instant scan that checks for all major security headers and provides an A+ through F grade
  • Mozilla Observatory — comprehensive analysis from Mozilla’s security team, covering headers plus additional security checks
  • Chrome DevTools — the Network tab shows all response headers for every request, letting you verify headers are applied correctly
  • CSP Evaluator (by Google) — specifically analyzes your CSP policy for weaknesses like overly broad source lists or missing directives

Integrate header checks into your monitoring and observability pipeline. Automated scans that run after every deployment catch configuration regressions before users encounter them. Set up CSP violation reporting (using the report-uri or report-to directive) to collect real-time data about policy violations in production.

Common Implementation Mistakes

Several patterns consistently cause problems when deploying security headers:

  • Using unsafe-inline in CSP — negates most XSS protection. Use nonces or hashes instead
  • Setting Access-Control-Allow-Origin: * with credentials — browsers reject this combination. Specify an exact origin when credentials are involved
  • Deploying HSTS with includeSubDomains before verifying all subdomains support HTTPS — can make subdomains inaccessible
  • Forgetting the always parameter in Nginx — without it, headers only apply to 2xx responses, leaving error pages unprotected
  • Not testing CSP in report-only mode first — enforcing an untested policy will break legitimate functionality

Teams managing web projects at scale benefit from centralizing security header configuration in their infrastructure layer. Tools like Toimi help coordinate these cross-cutting concerns across development teams, ensuring that security standards are documented, tracked, and consistently applied across all projects.

Security Headers and Application Architecture

Your application architecture influences where you implement security headers. Monolithic applications configure headers at the web server level (Nginx, Apache). Microservice architectures require headers at the API gateway or reverse proxy, ensuring consistent enforcement across all services.

Serverless architectures present unique challenges — each function may need its own CORS configuration, and there’s no central Nginx config to manage. Most serverless platforms (AWS Lambda with API Gateway, Cloudflare Workers, Vercel Edge Functions) support custom response headers through their configuration files, but the setup differs for each platform.

For applications that rely on authentication and authorization, security headers add a critical defense layer. CSP prevents stolen session tokens via XSS, HSTS prevents session cookies from being sent over HTTP, and frame-ancestors prevents clickjacking attacks that target authenticated actions.

Performance-conscious teams should note the interaction between security headers and Core Web Vitals. Overly restrictive CSP policies that block critical render path resources can increase Largest Contentful Paint, while HSTS eliminates HTTP-to-HTTPS redirect latency for repeat visitors.

Beyond the Basics: Advanced Headers

Several newer headers address emerging security concerns. Cross-Origin-Embedder-Policy (COEP) controls which cross-origin resources can be loaded and is required for enabling SharedArrayBuffer, which browsers restrict to mitigate Spectre-class attacks. Cross-Origin-Opener-Policy (COOP) isolates your browsing context from cross-origin windows, preventing other windows from obtaining references via window.opener. Cross-Origin-Resource-Policy (CORP) lets you declare whether other origins can include your resources, preventing hotlinking. Chrome and Firefox now require cross-origin isolation (COEP + COOP) before exposing powerful multi-threaded APIs.

A Pragmatic Implementation Roadmap

Don’t deploy all security headers at once. Follow this incremental approach:

  1. Week 1: Deploy X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin, and Permissions-Policy. These are low-risk, high-value headers with no configuration complexity
  2. Week 2: Deploy HSTS with a short max-age (300 seconds). Verify all pages and subdomains work over HTTPS. Gradually increase max-age to one year
  3. Week 3-4: Deploy CSP in report-only mode. Monitor violations. Adjust the policy until legitimate resources stop triggering violations
  4. Week 5: Switch CSP to enforcement mode. Continue monitoring violations for regressions
  5. Week 6: Review and configure CORS policies on all APIs. Audit cross-origin requests and tighten Access-Control-Allow-Origin to specific origins
  6. Ongoing: Review violation reports weekly. Update policies when adding new third-party scripts or CDN resources. Re-test after infrastructure changes

Each step is independently valuable — even deploying just the week-one headers significantly reduces your attack surface.

For teams managing multiple web properties, tracking this rollout across projects requires coordination. Taskee provides a straightforward way to create deployment checklists and track progress across your team’s security header implementation.

Frequently Asked Questions

What is Content Security Policy (CSP) and why is it important?

Content Security Policy is an HTTP response header that tells browsers which sources of content (scripts, styles, images, fonts) are permitted on your page. CSP is the most effective defense against cross-site scripting (XSS) attacks because it prevents browsers from executing injected scripts even if an attacker finds an injection point in your application. Deploy CSP in report-only mode first to discover what your page loads, then switch to enforcement mode after adjusting the policy.

What is the difference between CORS and CSP?

CSP and CORS address different security concerns. CSP controls what resources your page can load — it restricts scripts, styles, images, and other content to whitelisted sources. CORS controls which external websites can make requests to your server — it manages cross-origin API access. CSP protects your users from malicious content on your page, while CORS protects your API from unauthorized access by other websites. Most applications need both configured correctly.

How do I implement HSTS without breaking my site?

Start with a short max-age value (300 seconds or 5 minutes) and verify that every page and subdomain works correctly over HTTPS. Gradually increase max-age to 604800 (one week), then 2592000 (one month), and finally 31536000 (one year). Only add the includeSubDomains directive after confirming all subdomains support HTTPS. Only add the preload directive when you are certain about your HTTPS configuration, as HSTS preloading is very difficult to undo.

Which security headers should I implement first?

Start with the headers that require zero configuration and carry no risk of breaking your site: X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin, and Permissions-Policy with all unused features disabled. These four headers can be deployed in minutes and immediately reduce your attack surface. Follow up with HSTS (start with short max-age), then tackle CSP in report-only mode.

Why should I avoid using ‘unsafe-inline’ and ‘unsafe-eval’ in CSP?

The unsafe-inline directive allows execution of any inline script or style on your page, which means an attacker who injects a script tag through an XSS vulnerability can execute arbitrary code — defeating the primary purpose of CSP. The unsafe-eval directive permits the use of eval() and similar dynamic code execution functions, which attackers commonly exploit. Instead, use nonce-based or hash-based CSP that permits only specific, known inline scripts while blocking all others.