Secure APIs with HTTP Security Headers
How to configure essential security headers like HSTS, CSP, and X-Frame-Options to protect APIs and web applications from common attacks.
Overview
HTTP security headers are response metadata that tells browsers how to handle your content: cheap server-side defense that costs nothing in application code and protect against entire classes of attacks: clickjacking via X-Frame-Options, cross-site scripting via Content-Security-Policy, protocol downgrade attacks via Strict-Transport-Security, and MIME-type sniffing via X-Content-Type-Options.
The OWASP Secure Headers Project keeps a dedicated reference for security headers because they’re useful, easy to implement, and frequently forgotten during deployments. A server missing these headers isn’t immediately vulnerable, but it’s considerably less resilient against common web attacks. For broader application hardening, see the security best practices guide.
When to Use
Use this recipe when:
- Launching a new web application or API to production
- Conducting security audits or penetration tests
- Hardening existing applications after a security review
- Configuring reverse proxies (Nginx, Apache, CloudFront, Cloudflare)
- Building middleware for Express, FastAPI, or Spring Boot applications
Skip these if your consumers are all machine-to-machine or native apps — headers are browser instructions and a non-browser client ignores them.
Solution
Express.js Middleware
const helmet = require('helmet');
const express = require('express');
const app = express();
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://trusted-cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
strictTransportSecurity: {
maxAge: 63072000, // 2 years, per OWASP guidance
includeSubDomains: true,
},
}));
// Helmet's defaults already set COOP, COEP, CORP, Origin-Agent-Cluster
// and X-Content-Type-Options, so you only customize what differs.
Nginx Configuration
server {
listen 443 ssl;
server_name api.example.com;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
}
FastAPI (Python)
from fastapi import FastAPI
from starlette.middleware.base import BaseHTTPMiddleware
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return response
app = FastAPI()
app.add_middleware(SecurityHeadersMiddleware)
Explanation
- Strict-Transport-Security (HSTS): Forces the browser onto HTTPS for your domain. Prevents SSL stripping attacks where a man-in-the-middle downgrades the connection to HTTP. Keep
preloadout of the config until every subdomain serves HTTPS; once you’re on the preload list, removal is slow. - Content-Security-Policy (CSP): An allowlist covering where scripts, styles, images, and other resources may load from. Start tight: a strict CSP stops inline scripts and unknown domains, so injected markup can’t phone home even if an attacker gets it onto the page. For a JSON-only API,
default-src 'none'is enough. - X-Frame-Options: Keeps your site out of
<iframe>s on other domains — clickjacking needs the embed: an invisible frame puts your buttons under the attacker’s overlay. CSP’sframe-ancestorsdirective covers the same ground with more control. - X-Content-Type-Options: With
nosniff, the browser refuses to reinterpret a file as a different MIME type than the server declared, the gap that turns a user-uploaded.txtinto executable JavaScript. - Referrer-Policy: Decides how much of the URL leaks into the
Refererheader on outgoing requests. Withstrict-origin-when-cross-origin, full URLs (including tokens in query strings) only travel same-origin. - Permissions-Policy: Shuts down browser features you don’t need — an empty allowlist kills camera, microphone, geolocation, and payment for a few bytes per response.
- Cross-Origin-Opener/Resource/Embedder-Policy: The COOP/COEP/CORP trio isolates your browsing context and declares which origins may embed your resources, blocking Spectre-style cross-origin data leaks.
- Cache-Control: Not a “security header” in the strict sense, but
no-storeon responses that carry tokens or personal data keeps them out of shared and browser caches.
Here’s the trip a header makes from your server to the browser’s policy engine:
Variants
| Header | Attack Prevented | Required? | Browser Support |
|---|---|---|---|
| HSTS | SSL stripping | Yes | Universal |
| CSP | XSS, data injection | Yes | Universal |
| X-Frame-Options | Clickjacking | Yes | Universal |
| X-Content-Type-Options | MIME sniffing | Yes | Universal |
| Referrer-Policy | Information leakage | Recommended | Universal |
| Permissions-Policy | Feature abuse | Recommended | Modern |
| Cross-Origin-Opener-Policy | Context leaks (Spectre) | Recommended | Modern |
| Cross-Origin-Resource-Policy | Unauthorized embedding | Recommended | Modern |
| Clear-Site-Data | Stale session data | Logout responses | Modern |
| Cache-Control: no-store | Cached sensitive data | Yes, for APIs | Universal |
Skip the deprecated ones: X-XSS-Protection (removed from browsers, enabled vulnerabilities) and Feature-Policy (the old name for Permissions-Policy).
What works
- Use Helmet as a baseline: the Helmet middleware for Express sets sensible defaults for all major headers with a single line of code.
- Start with a restrictive CSP and relax gradually: begin with
default-src 'self'and add domains only when functionality breaks. A too-permissive CSP is almost worthless. - Treat HSTS preload as a separate decision: once every subdomain serves HTTPS, add the
preloaddirective and submit to hstspreload.org. Don’t enable it by default; removal is slow and breaks any subdomain that still needs HTTP. - Include headers on all responses: error pages (404, 500) and API responses should include the same headers as HTML pages. Attackers target error pages too.
- Test with securityheaders.com or Mozilla Observatory: these tools scan your site and grade your header configuration with specific remediation steps.
Common Mistakes
- Using
ALLOW-FROMin X-Frame-Options: modern browsers don’t support this value. UseSAMEORIGINorDENYinstead. - Enabling
unsafe-inlinefor scripts in CSP: it switches off the XSS protection CSP exists for. Use nonces or hashes when inline scripts are unavoidable. - Forgetting API endpoints: teams configure the headers on HTML routes and leave JSON responses bare. Apply them globally; an attacker doesn’t care which content type they hit.
- Setting HSTS without HTTPS ready: if part of your site still serves HTTP, HSTS breaks it for anyone who already visited the HTTPS version.
Advanced Solutions
Spring Boot security headers configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityHeadersConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives(
"default-src 'self'; " +
"script-src 'self' https://cdn.example.com; " +
"style-src 'self' https://fonts.googleapis.com; " +
"img-src 'self' data: https:; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"object-src 'none'"
))
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(63072000)
.includeSubDomains(true)
)
.contentTypeOptions(opts -> {})
.frameOptions(frame -> frame.deny())
.referrerPolicy(referrer -> referrer.policy(
org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN
))
.permissionsPolicy(permissions -> permissions.policy(
"geolocation=(), microphone=(), camera=(), payment=()"
))
);
return http.build();
}
}
Cloudflare Workers header injection
Inject security headers at the edge without touching origin infrastructure:
export default {
async fetch(request, env) {
const response = await fetch(request);
// Clone the response so we can modify headers
const newResponse = new Response(response.body, response);
// Security headers
newResponse.headers.set('Strict-Transport-Security',
'max-age=63072000; includeSubDomains');
newResponse.headers.set('X-Content-Type-Options', 'nosniff');
newResponse.headers.set('X-Frame-Options', 'DENY');
newResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
newResponse.headers.set('Permissions-Policy',
'geolocation=(), microphone=(), camera=()');
// Only set CSP for HTML responses
const contentType = response.headers.get('Content-Type') || '';
if (contentType.includes('text/html')) {
newResponse.headers.set('Content-Security-Policy',
"default-src 'self'; script-src 'self' https://cdn.example.com; " +
"style-src 'self' https://fonts.googleapis.com; " +
"img-src 'self' data: https:; connect-src 'self' https://api.example.com");
}
return newResponse;
},
};
CORS preflight with security headers
Combine CORS and security headers for cross-origin APIs:
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const app = express();
// Security headers first
app.use(helmet());
// CORS with specific origin allowlist
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
exposedHeaders: ['X-Request-ID', 'X-RateLimit-Remaining'],
credentials: true,
maxAge: 86400, // Cache preflight for 24 hours
};
app.use(cors(corsOptions));
// Explicit preflight handler
app.options('*', cors(corsOptions));
// API routes
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
Automated header testing with curl
#!/bin/bash
# audit-headers.sh — Check security headers on a URL
URL="${1:-https://example.com}"
REQUIRED_HEADERS=(
"strict-transport-security"
"content-security-policy"
"x-content-type-options"
"x-frame-options"
"referrer-policy"
)
echo "Auditing: $URL"
echo "-----------------------------------"
HEADERS=$(curl -sI "$URL")
for header in "${REQUIRED_HEADERS[@]}"; do
VALUE=$(echo "$HEADERS" | grep -i "^$header:" | sed 's/^[^:]*: *//')
if [ -z "$VALUE" ]; then
echo "MISSING: $header"
else
echo "OK: $header = $VALUE"
fi
done
# Check for weak CSP
CSP=$(echo "$HEADERS" | grep -i "^content-security-policy:" | sed 's/^[^:]*: *//')
if echo "$CSP" | grep -q "unsafe-inline"; then
echo "WARNING: CSP contains 'unsafe-inline'"
fi
if echo "$CSP" | grep -q "unsafe-eval"; then
echo "WARNING: CSP contains 'unsafe-eval'"
fi
# Check HSTS max-age
HSTS=$(echo "$HEADERS" | grep -i "^strict-transport-security:" | sed 's/^[^:]*: *//')
if echo "$HSTS" | grep -q "max-age=0"; then
echo "WARNING: HSTS max-age is 0 (disabled)"
fi
See Also
- Companion resources:
audit-headers.sh, a runnable Bash script that audits a URL for missing headers and weak CSP. - OWASP Secure Headers Project: the reference list of recommended headers and values, kept current.
- MDN HTTP headers: per-header syntax and browser support.
- hstspreload.org: check eligibility and submit your domain to the preload list.
Frequently Asked Questions
Do security headers protect APIs consumed by mobile apps?
Not really — they're browser instructions; a native app's HTTP client ignores CSP or X-Frame-Options entirely. For API-to-app traffic, your defenses are authentication, input validation, and TLS.
Can I set security headers in a CDN like Cloudflare?
Yes — often the best option when you can't touch the origin. Cloudflare Transform Rules and AWS CloudFront Functions inject headers at the edge, handy for static sites or legacy apps you can't redeploy.
What is the difference between CSP and CORS?
They solve different problems. CSP tells the browser which resources it may load while rendering your page; CORS tells it which other origins may call your API. Most apps need both.
Should I use report-uri in CSP?
It's on the way out in favor of report-to and the Reporting API, though browsers still honor it for now. During rollout, pair either one with Content-Security-Policy-Report-Only to collect violations without blocking anything; enforce once the policy is clean.
Do I need CSP on a JSON-only API?
A minimal one. default-src 'none' plus frame-ancestors 'none' costs a few bytes and shuts down resource loading and framing on responses that never use browser features.
Should headers be set in the proxy or the application?
Prefer the edge. Setting them in Nginx, a CDN, or an API gateway covers every response uniformly, including error pages your app might not control. Keep app-level middleware as the fallback for headers that depend on route context, like CSP nonces.
Related Resources
Prevent Cross-Site Scripting (XSS)
How to sanitize user input, escape output, and use Content Security Policy to prevent XSS attacks in web applications.
RecipePrevent SQL Injection Attacks
How to write parameterized queries and use ORMs to eliminate SQL injection vulnerabilities across Python, JavaScript, and Java.
RecipeHandle CORS in Express, Flask and Spring Boot
How to configure Cross-Origin Resource Sharing (CORS) headers correctly for APIs, SPAs, and serverless functions without opening security holes.
RecipeRate Limiting for API Security: Block Abuse and Brute Force
Use rate limiting as a security control against brute-force, credential stuffing, scraping, and L7 DDoS. Layered limits, per-endpoint tuning, and 429 handling.
RecipeHMAC Request Signing for API Authentication (AWS Sig v4)
Secure API requests with HMAC signatures and AWS Signature v4 authentication for tamper-proof message integrity.
RecipeConfigure HTTP Security Headers with Helmet in Node.js
Set security HTTP headers in Express apps with Helmet — CSP, HSTS, X-Frame-Options, X-Content-Type-Options, and CORS for OWASP-compliant web security