HTTP Security Headers Explained: A Practical Guide to CSP, HSTS, and More

HTTP security headers are instructions sent by a web server with its response. They tell the browser how to handle transport security, scripts, frames, content types, referrer data, cookies, and powerful browser features. A well-designed header policy can reduce the impact of cross-site scripting, clickjacking, MIME confusion, information leakage, and accidental use of insecure connections.

Headers are defense in depth, not a substitute for secure code. They cannot repair broken authorization, vulnerable dependencies, unsafe database queries, or exposed secrets. They can, however, remove entire classes of avoidable browser behavior and make other vulnerabilities harder to exploit.

This guide explains the most useful headers, gives cautious starting configurations for Nginx and Apache, and shows how to test changes before enforcing them in production.

Security headers at a glance

Header or directive Primary purpose Practical starting point
Content-Security-Policy Restricts where scripts, styles, images, frames, and other resources can load from Inventory sources and begin with Report-Only
Strict-Transport-Security Tells browsers to use HTTPS for future requests Start with a short max-age, then increase it
X-Content-Type-Options Prevents MIME type sniffing nosniff
Referrer-Policy Limits referrer information sent with requests strict-origin-when-cross-origin
Permissions-Policy Controls access to browser features Disable unused features explicitly
CSP frame-ancestors Controls which sites may embed a page 'none' or 'self'
X-Frame-Options Legacy clickjacking protection DENY or SAMEORIGIN
Set-Cookie attributes Protects session and application cookies Secure, HttpOnly, appropriate SameSite

1. Content-Security-Policy: control what a page may load

Content Security Policy, usually shortened to CSP, tells a browser which resource locations are allowed. It can restrict scripts, styles, images, fonts, frames, form destinations, network connections, media, workers, and other content. A strong CSP is one of the most valuable browser-side defenses against cross-site scripting and injected content.

A small baseline policy might look like this:

Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests

This is a starting point, not a universal answer. It will block third-party scripts, styles, fonts, images, analytics, payment widgets, embedded videos, and API connections unless their sources are added to the appropriate directives. WordPress sites in particular may use inline styles, plugin scripts, external fonts, and administrative resources that require careful policy design.

Start with Report-Only

Do not paste a restrictive CSP into a busy production site and hope for the best. First send the same policy through Content-Security-Policy-Report-Only. Browsers will report violations without blocking the resources. Review the browser console and collected reports, identify legitimate dependencies, and refine the policy before switching to enforcement.

Content-Security-Policy-Report-Only: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"

For stronger script protection, mature applications should prefer nonces or hashes over broad allowances such as 'unsafe-inline'. A nonce must be unpredictable and generated separately for every response, so it normally requires application-level support rather than a static server directive.

2. Strict-Transport-Security: require HTTPS on future visits

HTTP Strict Transport Security, or HSTS, tells a browser that a hostname should only be accessed over HTTPS. After receiving the header on a secure response, the browser automatically upgrades later HTTP attempts and refuses to let the user click through certificate errors.

A production policy commonly looks like:

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

HSTS is powerful because it persists in the browser. That persistence also makes mistakes painful. Before using includeSubDomains, verify that every current and future subdomain supports HTTPS correctly. Before requesting preload status, understand that removal is not immediate and ensure the entire domain is ready for a long-term HTTPS-only commitment.

A safer rollout is to begin with a short value such as max-age=300, monitor the site, increase it to a day or a week, and only then move to a year. Add includeSubDomains separately after auditing subdomains. Browsers ignore HSTS sent over HTTP, so configure the header on HTTPS responses and redirect plain HTTP traffic to HTTPS.

3. frame-ancestors and X-Frame-Options: prevent unwanted framing

An attacker can place a transparent or disguised version of a page inside a frame and trick a user into clicking its controls. This is known as clickjacking. The modern defense is CSP’s frame-ancestors directive:

Content-Security-Policy: frame-ancestors 'none'

Use 'self' instead if pages must be embedded by the same origin. Specific trusted origins can also be listed when framing is an intentional product feature.

X-Frame-Options: DENY provides a useful fallback for older browser behavior. Use SAMEORIGIN when same-origin framing is required. The obsolete ALLOW-FROM value is not a reliable way to permit selected origins; use CSP frame-ancestors for that purpose.

4. X-Content-Type-Options: stop MIME guessing

Browsers sometimes try to infer a resource’s type instead of strictly following its declared Content-Type. That behavior can turn incorrectly served content into executable script or HTML. The remedy is simple:

X-Content-Type-Options: nosniff

This header does not excuse incorrect MIME types. JavaScript must still be served with a JavaScript media type, CSS with text/css, and HTML with an appropriate content type and character encoding. Test uploaded and generated files carefully after enabling nosniff.

5. Referrer-Policy: limit information shared during navigation

The Referer request header can reveal the page a visitor came from. Depending on the policy, it may include a path and query string containing internal identifiers or other information that should not be sent to another site.

A balanced default for many public websites is:

Referrer-Policy: strict-origin-when-cross-origin

This preserves useful same-origin referrer information, sends only the origin on secure cross-origin requests, and sends no referrer when navigating from HTTPS to HTTP. Applications with stricter privacy requirements may choose no-referrer, while analytics-heavy sites should verify how a change affects attribution.

6. Permissions-Policy: disable browser features you do not use

Permissions Policy limits access to browser features in the current document and embedded frames. A site that never needs the camera, microphone, or geolocation can disable them explicitly:

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

Do not blindly copy a very long feature list. Browser support and recognized directives evolve, and embedded applications may legitimately need selected capabilities. Begin with sensitive features you know the site does not use, then test payment flows, video calls, maps, file importers, and third-party frames.

7. Cookies and caching are part of the header review

Set-Cookie is not usually described as a security-header scanner category, but its attributes are critical. Session cookies should normally use Secure so they are sent only over HTTPS and HttpOnly so normal JavaScript cannot read them. Choose SameSite=Lax, Strict, or None according to the authentication and cross-site workflow. Cookies using SameSite=None must also use Secure.

Caching must also match the sensitivity of the response. Use Cache-Control: no-store when a response containing confidential data must not be stored. Remember that no-cache permits storage but requires revalidation before reuse; it is not equivalent to no-store.

A cautious Nginx configuration

The following example provides a reasonable starting structure. The CSP must be customized, and HSTS should remain disabled until HTTPS readiness has been confirmed.

# Send headers on error responses as well as successful responses.
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

# Test this policy with Content-Security-Policy-Report-Only first.
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests" always;

# Enable only after verifying HTTPS on the hostname and relevant subdomains.
# Start with max-age=300 before increasing the duration.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

The always parameter matters because otherwise Nginx sends add_header values only for selected response status codes. Also review header inheritance: defining any add_header directive inside a nested location can change which headers are inherited from a higher configuration level.

A cautious Apache configuration

Apache can set the same baseline through mod_headers. Place this in the relevant virtual host when possible, or in .htaccess when overrides are permitted.

<IfModule mod_headers.c>
  Header always set X-Content-Type-Options "nosniff"
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
  Header always set X-Frame-Options "DENY"
  Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
  Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests"

  # Enable only after a staged HTTPS readiness test.
  Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>

Apache maintains different internal header tables for ordinary and always behavior. If an application, proxy, or earlier rule already sets the same header, the browser may receive duplicates. Inspect the final response and unset an existing value before setting a replacement when necessary.

What about WordPress and Cloudflare?

For WordPress, prefer setting global security headers at the outermost reliable layer: the web server, reverse proxy, or CDN. Application code may not run for static files, cached pages, redirects, or server-generated error responses. A plugin or PHP hook can still be appropriate for dynamic, application-specific policies, especially a nonce-based CSP, but verify every response path.

Cloudflare response-header transform rules can add or replace headers at the edge. This is useful when origin access is limited, but it also introduces another configuration layer. Decide which layer owns each header. Avoid setting one CSP at the origin and a different CSP at the CDN, because multiple CSP headers are combined restrictively rather than allowing the union of both policies.

How to test HTTP security headers

Start with the response a real visitor receives, not only the server configuration file. A quick command-line check is:

curl -I https://example.com/

# Follow redirects and show the final response headers.
curl -IL https://example.com/

Then check several response types: the homepage, a normal content page, a redirect, a 404 page, an authenticated page, an API response, and representative static assets. Use browser developer tools to look for blocked resources and CSP console messages.

A focused scanner such as HeaderSec can provide a grade, prioritized findings, evidence, and remediation guidance after the manual check. Running a second scanner is useful because tools may weigh policies differently. Our comparison of the best SecurityHeaders.com alternatives covers options for header scanning, continuous monitoring, TLS analysis, and CI.

Common security header mistakes

  • Enforcing CSP before observing: deploy Report-Only first and review legitimate violations.
  • Adding unsafe-inline everywhere: this can remove much of CSP’s script protection. Prefer nonces or hashes when the application supports them.
  • Preloading HSTS too early: confirm every subdomain and certificate process before making a hard-to-reverse commitment.
  • Sending HSTS over HTTP: browsers accept it only from a secure HTTPS response.
  • Breaking intentional embeds: choose frame-ancestors and X-Frame-Options values that match real framing requirements.
  • Duplicating headers: origin, application, proxy, and CDN layers may each append their own value.
  • Testing only HTTP 200: redirects, authentication errors, 404s, and 500 responses need appropriate protection too.
  • Copying a scanner’s ideal policy blindly: the correct policy depends on the application’s assets, integrations, and threat model.
  • Keeping obsolete headers: HPKP and Expect-CT should not be added to a modern baseline, and X-XSS-Protection is generally omitted or explicitly disabled in favor of CSP.

A practical deployment checklist

  1. Inventory domains, subdomains, third-party assets, frames, APIs, and browser features.
  2. Capture current production response headers from several URL and status-code types.
  3. Add low-risk headers such as nosniff and an intentional referrer policy.
  4. Deploy CSP in Report-Only mode and collect representative violations.
  5. Refine and enforce CSP in stages, monitoring errors and business flows.
  6. Roll out HSTS with a short max-age, then increase it after verification.
  7. Add includeSubDomains only after auditing the entire namespace.
  8. Retest from outside the origin so CDN and proxy changes are included.
  9. Automate recurring checks to catch configuration drift.

Frequently asked questions

Which HTTP security header should I add first?

X-Content-Type-Options: nosniff is usually a low-risk first step if the site already serves correct MIME types. An intentional Referrer-Policy is also commonly straightforward. CSP and HSTS provide major benefits but require a staged rollout.

Will security headers stop all XSS attacks?

No. A strong CSP can reduce the exploitability of many injection flaws, but the application must still validate input, encode output, avoid unsafe DOM operations, and keep dependencies secure.

Should every site use HSTS preload?

No. Preload is appropriate only when the registrable domain and all subdomains are ready for permanent HTTPS enforcement and the team understands the operational commitment. Ordinary HSTS can be deployed without preload.

Why do two scanners give different grades?

Scanners use different rulesets, weights, browser assumptions, and opinions about legacy headers. Review the evidence behind each finding instead of optimizing only for a letter grade.

Final thoughts

A good security-header configuration is deliberate, observable, and owned by one clear layer of the stack. Begin with the controls that match the site’s real behavior, test CSP without enforcement, roll out HSTS gradually, and inspect the final response after every proxy and cache has handled it.

The goal is not to collect the largest possible set of headers. It is to give browsers a coherent policy that reduces risk without breaking legitimate users or application features.

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x