15.14 Security Headers
Table of Contents
Why Security Headers Matter
Security headers are small pieces of information that the server sends with HTTP responses. Browsers read these headers and change how they behave. With a few lines of configuration you can:
- Block many common attacks before they start
- Reduce the damage if something goes wrong
- Add extra checks on every request, even if your application code has bugs
Security headers are defense in depth, not a replacement for secure code.
You must still validate input, escape output, and fix vulnerabilities in your app.
We will look at the most important headers backend developers should know, how they work, and simple examples.
Common Security Headers Overview
Here is a quick overview of important security headers you will see or use:
| Header name | Main purpose |
|---|---|
Content-Security-Policy | Limits what resources / scripts can run |
X-Frame-Options | Controls if the site can be inside an iframe |
X-Content-Type-Options | Prevents content type sniffing |
Referrer-Policy | Controls how much referrer info is sent |
Strict-Transport-Security | Forces HTTPS in the browser |
X-XSS-Protection | Legacy XSS filter control |
Permissions-Policy | Controls access to powerful browser features |
Cross-Origin-Opener-Policy | Isolation for security and performance |
Cross-Origin-Resource-Policy | Controls cross origin resource loading |
Cross-Origin-Embedder-Policy | Stronger isolation for some apps |
Access-Control-* | CORS headers, control cross origin requests |
We will focus on the ones that matter most for typical backend APIs and web apps.
X-Content-Type-Options
Browsers try to guess the content type of a response. Sometimes they ignore the Content-Type header and "sniff" the type from the content. This can be dangerous.
For example:
- You send a text file but the browser guesses it is HTML with JavaScript
- An attacker uploads a "image" file that actually contains HTML and script
- The browser treats it as executable content and runs the script
X-Content-Type-Options tells the browser not to guess and to trust your Content-Type header.
Typical value:
X-Content-Type-Options: nosniff
Always set
X-Content-Type-Options: nosniff
on all responses that return user controlled content or file downloads.
Example: Simple HTTP response
HTTP/1.1 200 OK
Content-Type: text/css
X-Content-Type-Options: nosniff
body { background: #fff; }Even if the CSS file contains something that might look like HTML or JavaScript, the browser will treat it only as CSS.
X-Frame-Options
Attackers can put your site inside an <iframe> on their own site and trick users. This is called a clickjacking attack. The user thinks they click a harmless button, but they actually click something on your site, for example "Delete account".
X-Frame-Options tells the browser if your site is allowed to be loaded inside an iframe.
Common values:
| Value | Meaning |
|---|---|
DENY | Never allow this page to be inside any frame/iframe |
SAMEORIGIN | Only allow in a frame on the same origin |
ALLOW-FROM uri | Allow in a frame only from specific origin (legacy, not well supported) |
Typical safe choice for most apps:
X-Frame-Options: SAMEORIGINIf your app never needs to be in an iframe at all, you can use:
X-Frame-Options: DENYExample: Protecting a login page
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: DENY
<html>
<body>Login form here...</body>
</html>Now another site cannot embed your login page in an invisible iframe and trick the user.
Strict-Transport-Security (HSTS)
If your site uses HTTPS, you still have a small problem. A user might type http://example.com or click an old http link. The browser first connects with HTTP, then you redirect to HTTPS. That first HTTP request can be attacked, for example:
- An attacker removes the redirect
- Or replaces it with their own fake page
Strict-Transport-Security (HSTS) tells the browser:
For the next N seconds, only use HTTPS for this domain.
Once the browser sees this header, it will:
- Automatically convert
http://example.comtohttps://example.com - Refuse to connect over plain HTTP during the HSTS period
Typical value:
Strict-Transport-Security: max-age=31536000; includeSubDomainsExplanation:
max-age=31536000means 31536000 seconds, about 1 yearincludeSubDomainsapplies it to all subdomains, for exampleapi.example.com
Only enable HSTS after your site is correctly served over HTTPS.
If you misconfigure it with a long max-age, you can lock users out until it expires.
Example: Response with HSTS
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
The optional preload directive is used when you submit your domain to the browser vendors' HSTS preload list. That is more advanced and must be done carefully.
Content-Security-Policy (CSP)
Content-Security-Policy is one of the most powerful security headers. It tells the browser what it is allowed to load and execute.
You can use CSP to:
- Block inline JavaScript
- Limit scripts to your own domain
- Control where images, styles, fonts, and other resources can come from
- Prevent many types of XSS (cross-site scripting) attacks or reduce their impact
General structure:
Content-Security-Policy: <directive1> <value>; <directive2> <value>; ...Each directive controls a type of content.
Common directives:
| Directive | Controls | Example value |
|---|---|---|
default-src | Fallback for all content types | 'self' |
script-src | JavaScript sources | 'self' https://cdn.example.com |
style-src | CSS sources | 'self' 'unsafe-inline' |
img-src | Image sources | 'self' data: |
connect-src | AJAX, WebSocket, fetch connections | 'self' https://api.example.com |
frame-ancestors | Who can embed this page in a frame | 'self' or none |
Some important keywords:
'self'means same origin as the page'none'means disallow completely'unsafe-inline'allows inline<script>or inline styles (not recommended if you want strong protection)
A strong CSP often breaks existing pages that use inline scripts or styles.
Start with report only mode to test CSP:
Content-Security-Policy-Report-Only: ...
Example: Basic CSP for a simple app
You have an app that:
- Serves HTML, CSS, JS from the same origin
- Loads images only from the same origin
- Makes AJAX requests only to the same origin
You can start with:
Content-Security-Policy: default-src 'self';This means: all content types (scripts, styles, images, etc.) can load only from the same origin.
Example: CSP for app with CDN
You serve scripts and styles from https://cdn.example.com and images from anywhere (user avatars, external sites).
Content-Security-Policy: default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' https://cdn.example.com;
img-src 'self' https:;Broken into readable form:
Content-Security-Policy: default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' https://cdn.example.com;
img-src 'self' https:;Here:
- Scripts and styles are allowed from your main site and the CDN
- Images are allowed from any HTTPS URL
Referrer-Policy
When you click a link, the browser often sends a Referer header (note the misspelling) to the new site, containing the URL of the page you came from.
Example:
Referer: https://example.com/account/settings?token=SECRETThis can leak:
- Sensitive URLs
- Query parameters with tokens or IDs
- Internal path structure
Referrer-Policy controls how much information is sent.
Common values:
| Value | Behavior |
|---|---|
no-referrer | Never send Referer |
no-referrer-when-downgrade | Default in many browsers. No referrer when going HTTPS → HTTP |
origin | Send only origin, for example https://example.com |
origin-when-cross-origin | Full URL on same origin, only origin to other sites |
strict-origin | Only origin, and only for HTTPS → HTTPS |
strict-origin-when-cross-origin | Good balanced default in modern apps |
A safe and common choice:
Referrer-Policy: strict-origin-when-cross-originThis:
- Sends full URL when navigating within your own site
- Sends only origin to other sites
- Avoids sending referrer from HTTPS to HTTP
Example: Response with referrer policy
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Referrer-Policy: strict-origin-when-cross-originPermissions-Policy
Modern browsers have many powerful features:
- Camera
- Microphone
- Geolocation
- Notifications
- Fullscreen
- Clipboard access
Permissions-Policy lets you control which origins can use these features. It can limit them for your own pages and for any iframes.
The syntax is:
Permissions-Policy: feature1=(allowed-origins), feature2=(allowed-origins)
Examples for allowed-origins:
()means no one can use it*means any origin'self'means same origin
Use Permissions-Policy to turn off features you do not use.
Less available features mean less attack surface.
Example: Disable camera and microphone, allow geolocation only on same origin
Permissions-Policy: camera=(), microphone=(), geolocation=('self')Example: Disable all powerful features you do not need
For a simple content site that does not need any of these:
Permissions-Policy: camera=(), microphone=(), geolocation=(), fullscreen=('self'), payment=()You can adjust based on your app's needs.
Cross-Origin Security Headers
Modern browsers and apps often load resources from other origins. For example:
- A frontend app on
https://app.example.comcalls an API onhttps://api.example.com - A page loads images or scripts from third party sites
Several headers help protect cross origin interactions.
We will not go deep into CORS here, because it usually has its own topic, but we will connect it to security headers.
CORS (Access-Control-Allow-*)
CORS is controlled mainly with these headers:
Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Allow-Credentials
They decide which other origins can access your resources with JavaScript.
For example, to allow a frontend at https://app.example.com to call your API:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Avoid Access-Control-Allow-Origin: * for APIs that use cookies or credentials.
Always limit origins to the sites that should use your API.
Cross-Origin-Resource-Policy (CORP)
Cross-Origin-Resource-Policy controls who can load your resources.
Common values:
| Value | Meaning |
|---|---|
same-origin | Only same origin sites can load resource |
same-site | Only same site (including subdomains) |
cross-origin | Any origin can load |
For many APIs or private resources, you can use:
Cross-Origin-Resource-Policy: same-siteFor very strict protection:
Cross-Origin-Resource-Policy: same-originCross-Origin-Opener-Policy (COOP)
Cross-Origin-Opener-Policy controls how new windows and tabs interact with the opener. It is part of an isolation model to prevent side channel attacks.
Common values:
same-originsame-origin-allow-popupsunsafe-none(default, not recommended for some apps)
For many modern apps you will see:
Cross-Origin-Opener-Policy: same-originThis isolates your browsing context from others.
Cross-Origin-Embedder-Policy (COEP)
Cross-Origin-Embedder-Policy controls if your page can load cross origin resources that are not explicitly allowed. It is often used together with COOP for strong isolation.
Values:
unsafe-none(default)require-corp
For special use cases, like applications that use SharedArrayBuffer, you might see:
Cross-Origin-Embedder-Policy: require-corpFor basic backend development you mostly need to know that these exist and are related to advanced browser security models.
X-XSS-Protection
X-XSS-Protection was used to control old browser built-in XSS filters. Many modern browsers either ignore it or have removed these filters, because they sometimes caused other issues.
Common values:
0disable the XSS filter1enable the filter1; mode=blockblock the page if XSS is detected
Example:
X-XSS-Protection: 1; mode=blockToday:
- It can still help in older browsers
- It is not a replacement for real XSS protection and CSP
For most new apps, focus more on:
- Proper output encoding
- Input validation
- Strong
Content-Security-Policy
How to Add Security Headers in a Backend
The exact way to add headers depends on the framework and language. The ideas are the same:
- Decide which headers you want and their values
- Add them in a central place, usually middleware or global configuration
- Test that they appear in responses and do not break your app
Below are simple examples using generic pseudo code and Python / FastAPI style, but the concept applies to any backend.
Generic middleware pattern
Most frameworks have a concept similar to this:
function security_headers_middleware(request, next_handler):
response = next_handler(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=('self')"
return responseThen you register this middleware globally.
Example: FastAPI style middleware
If you use FastAPI, you could write:
from fastapi import FastAPI, Request
from fastapi.responses import Response
app = FastAPI()
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response: Response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=('self')"
# Only add HSTS if using HTTPS in production
# In development with HTTP, skip this header
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
return responseYou can customize CSP per route or apply a default:
CSP = "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';"
@app.middleware("http")
async def add_csp(request: Request, call_next):
response = await call_next(request)
response.headers["Content-Security-Policy"] = CSP
return responseYou will adapt this idea to your own framework: Express.js, Django, Laravel, Spring, etc.
Recommended Default Set
For a typical modern HTTPS backend with a browser based frontend, a common baseline is:
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=('self')
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self';
Cross-Origin-Resource-Policy: same-siteYou might need to adjust:
Content-Security-Policyto allow your CDN or third party resourcesPermissions-Policyif you use camera, microphone, or other features- HSTS settings when you first enable HTTPS
Always test new security headers in a staging environment first.
A wrong CSP or CORS configuration can break your frontend.
Testing and Tools
To check your security headers:
- Open browser dev tools, go to the "Network" tab
- Reload your page
- Click a request and look at the "Response Headers" section
You can also use online scanners:
- Mozilla Observatory
- securityheaders.com
They will:
- Show which headers are present
- Suggest improvements
By understanding and correctly using security headers, you give your backend an extra protective layer. Even small config changes, like adding X-Content-Type-Options: nosniff or X-Frame-Options: SAMEORIGIN, can block entire classes of attacks with almost no code changes.
Views: 8
KAHIBARO