What Are Security Headers?

HTTP security headers are directives sent by a web server in its response headers that instruct the browser to enable or disable specific security features. They act as an additional layer of defense, telling the browser how to behave when handling your site's content.

Think of them as security policies your server communicates to every visitor's browser. Without these headers, browsers use their default behavior, which is often permissive. By setting security headers, you restrict what the browser is allowed to do, significantly reducing the attack surface of your web application.

💡
Defense in Depth

Security headers do not replace secure coding practices -- they complement them. Even if your application has a vulnerability, properly configured headers can prevent or limit exploitation. This is the principle of defense in depth: multiple overlapping protections so that no single failure is catastrophic.

Security headers are configured on the web server (Nginx, Apache, IIS) or in your application code. They cost nothing to implement, require no client-side changes, and can dramatically improve your site's security posture.

Content-Security-Policy (CSP)

Content-Security-Policy is the most powerful security header available. It controls which resources the browser is allowed to load for your page -- scripts, stylesheets, images, fonts, frames, and more. CSP is the primary defense against Cross-Site Scripting (XSS) attacks.

How CSP Works

CSP uses directives to define allowed sources for each resource type. If a resource does not match the policy, the browser blocks it and logs a violation.

# Basic CSP header
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src *; frame-src 'none';

Key Directives

default-src Fallback policy for any resource type not explicitly defined. Start with 'self' to only allow resources from your own domain.
script-src Controls where JavaScript can be loaded from. Avoid 'unsafe-inline' and 'unsafe-eval' -- they defeat the purpose of CSP. Use nonces or hashes instead.
style-src Controls where CSS can be loaded from. 'unsafe-inline' is sometimes needed for legacy sites but should be avoided if possible.
img-src Controls where images can be loaded from. Use 'self' plus specific CDN domains.
frame-src Controls which domains can be embedded in iframes. Set to 'none' if you do not use iframes.
connect-src Controls which URLs the page can connect to via fetch, XMLHttpRequest, WebSocket, and EventSource.
⚠️
Start with Report-Only

A misconfigured CSP can break your site by blocking legitimate resources. Use Content-Security-Policy-Report-Only first to monitor violations without enforcing the policy. Once you are confident no legitimate resources are blocked, switch to the enforcing header.

Nginx Configuration Example

# In your Nginx server block
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;

X-Content-Type-Options

This header prevents browsers from MIME-type sniffing. Without it, a browser might interpret a file differently than the server intended. For example, an attacker could upload a file with a .jpg extension that actually contains JavaScript. If the browser sniffs the content and determines it is JavaScript, it might execute it.

X-Content-Type-Options: nosniff

The nosniff value tells the browser to strictly follow the Content-Type header sent by the server. If the server says a file is an image, the browser treats it as an image -- period. It will not try to guess the type from the content.

💡
Always set this header.

This is a single-value header with no configuration complexity. There is no reason not to include it. It prevents an entire class of attacks with zero risk of breaking your site.

X-Frame-Options

X-Frame-Options controls whether your page can be embedded inside an <iframe> on another site. This is critical for preventing clickjacking attacks, where an attacker overlays your page with transparent elements to trick users into clicking hidden buttons.

# Prevent any site from framing your page
X-Frame-Options: DENY

# Only allow your own site to frame your pages
X-Frame-Options: SAMEORIGIN

Clickjacking Example

Imagine a banking site that does not set X-Frame-Options. An attacker creates a page with an invisible iframe loading the bank's transfer page, positioned so the "Confirm Transfer" button aligns with a visible "Click here to win a prize" button. The user thinks they are clicking the prize button, but they are actually confirming a bank transfer.

With X-Frame-Options: DENY, the browser refuses to load the banking page inside the iframe, and the attack fails completely.

💡
CSP frame-ancestors vs. X-Frame-Options

The CSP directive frame-ancestors provides the same protection with more flexibility (you can whitelist specific domains). Modern browsers support both, but setting both ensures backward compatibility with older browsers.

Strict-Transport-Security (HSTS)

HSTS forces browsers to only communicate with your site over HTTPS. Once a browser receives an HSTS header, it will automatically convert any HTTP request to HTTPS for the specified duration, without ever making an insecure request.

# Enforce HTTPS for 1 year, including all subdomains
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Parameters

max-age How long (in seconds) the browser should remember to only use HTTPS. 31536000 = 1 year. Start with a shorter value (e.g., 86400 = 1 day) while testing.
includeSubDomains Applies the HTTPS-only rule to all subdomains. Only use this if ALL your subdomains support HTTPS.
preload Signals that you want to be included in browsers' built-in HSTS preload list. Submit at hstspreload.org after thorough testing.
⚠️
HSTS is difficult to undo.

Once a browser caches your HSTS policy, it will refuse plain HTTP connections until max-age expires. If you later need to serve HTTP for any reason, visitors who received the header will be unable to connect. Start with a short max-age and increase it only after confirming everything works.

Referrer-Policy

When a user clicks a link on your site to go to another site, the browser normally sends a Referer header containing the URL they came from. This can leak sensitive information such as query parameters, session tokens in URLs, or private page paths.

# Recommended: send origin only when navigating to another site
Referrer-Policy: strict-origin-when-cross-origin

Common Values

no-referrer Never send the Referer header. Maximum privacy but breaks analytics and some anti-CSRF protections.
same-origin Send the full URL only for same-origin requests. No referrer sent to external sites.
strict-origin-when-cross-origin Send the full URL for same-origin requests. For cross-origin requests, send only the origin (domain). Send nothing if downgrading from HTTPS to HTTP. This is the recommended default.
no-referrer-when-downgrade Send the full URL unless navigating from HTTPS to HTTP. This is the browser default if no policy is set.

Permissions-Policy

Permissions-Policy (formerly Feature-Policy) controls which browser features and APIs your page can use. This includes sensitive capabilities like the camera, microphone, geolocation, payment requests, and more.

# Disable sensitive features not needed by your site
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()

Each feature is set to a list of allowed origins. An empty list () disables the feature entirely. (self) allows only your own origin to use it. Specific domains can be listed for features you need from third-party embeds.

# Allow camera only for your domain and a specific video service
Permissions-Policy: camera=(self "https://video.example.com"), microphone=(), geolocation=()
💡
Principle of Least Privilege

Disable every feature you do not actively use. If your site has no reason to access the camera, microphone, or GPS, disable them explicitly. This prevents any injected script from accessing these APIs even if an XSS vulnerability exists.

Checking Your Headers

After configuring security headers, you need to verify they are being sent correctly. Here are several methods to check.

Using Browser Developer Tools

Open your browser's Developer Tools (F12), go to the Network tab, click on the initial page request, and look at the Response Headers section. All security headers you configured should appear there.

Using curl

# View all response headers
curl -I https://yoursite.com

# Check for specific headers
curl -sI https://yoursite.com | grep -iE "content-security|x-frame|x-content-type|strict-transport|referrer-policy|permissions-policy"

Online Scanners

Several free tools grade your security headers and provide recommendations:

  • securityheaders.com -- Scans your site and provides an A-F grade with detailed explanations for each missing or misconfigured header
  • Mozilla Observatory (observatory.mozilla.org) -- Comprehensive scan that checks headers plus additional security best practices
  • CSP Evaluator (csp-evaluator.withgoogle.com) -- Specifically analyzes your Content-Security-Policy for weaknesses

Complete Nginx Example

# Add to your Nginx server block or include file
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
⚠️
The always keyword matters.

In Nginx, add_header without always only sends the header on successful responses (2xx/3xx). Error pages (4xx/5xx) will not include your security headers. Always use the always parameter to ensure headers are sent on every response.

Summary

HTTP security headers are a low-cost, high-impact defense layer for any web application. Here is what you learned:

  • Content-Security-Policy -- Controls which resources the browser can load, preventing XSS and data injection attacks
  • X-Content-Type-Options -- Prevents MIME-type sniffing with a simple nosniff directive
  • X-Frame-Options -- Blocks clickjacking by preventing your pages from being embedded in iframes
  • Strict-Transport-Security -- Forces HTTPS connections, eliminating SSL stripping attacks
  • Referrer-Policy -- Controls how much URL information leaks when users navigate to other sites
  • Permissions-Policy -- Restricts which browser APIs (camera, mic, GPS) your page can access
  • Always verify your headers using browser tools, curl, or online scanners
🎉
Great work!

You now know how to harden web applications using HTTP security headers. These headers work alongside secure coding practices to create multiple layers of protection. Next, learn about Cross-Site Scripting (XSS) to understand one of the attacks these headers help prevent.