API Security Headers Explained: HSTS, CSP, X-Frame-Options and Why They Matter
API Security Headers Explained: HSTS, CSP, X-Frame-Options and Why They Matter
If you've ever run a security scan on your API, you've probably seen warnings about "missing security headers." This post explains what each header does, what attack it prevents, and how to add it in 30 seconds in any framework.
Why headers matter
HTTP response headers tell the browser how to handle your responses. By default, browsers do permissive things — load any frame, execute any script, allow any plain HTTP downgrade. Security headers tell the browser to do the safe thing instead.
Headers are the cheapest, fastest, highest-impact security improvement you can make. No code changes. No deploys (sometimes). Just a few lines of config.
1. Strict-Transport-Security (HSTS)
What it does
Forces browsers to ALWAYS use HTTPS for your domain, even if the user types http://.
What attack it prevents
SSL stripping. An attacker on the same network (coffee shop Wi-Fi, hotel, airport) intercepts the first plain HTTP request and serves a downgraded HTTP version of your site. Now they can read everything — passwords, tokens, session cookies.
What happens without it
Every user is one rogue Wi-Fi network away from being compromised. The first time they type your domain, the browser tries HTTP, the attacker intercepts.
How to add it
Nginx:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Express / Node.js:
const helmet = require('helmet');
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true
}));
Django:
# settings.py
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
Rails:
# config/environments/production.rb
config.force_ssl = true
max-age=31536000 is one year — the recommended value. Don't set it lower than a few months or browsers won't bother enforcing it.
2. Content-Security-Policy (CSP)
What it does
Tells the browser which resources (scripts, styles, images, frames) are allowed to load on your pages. Anything not in the allowlist is blocked.
What attack it prevents
Cross-Site Scripting (XSS). If an attacker manages to inject a <script> tag into your page — through a form field, URL parameter, stored data, or compromised dependency — the browser refuses to execute it. CSP is the difference between a minor input validation bug and a full account takeover.
What happens without it
Any XSS bug becomes critical. An attacker who can inject one line of JavaScript can read every cookie, session token, and credential the user has access to.
How to add it
Nginx:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" always;
Express / Node.js:
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
}
}));
Django:
# pip install django-csp
MIDDLEWARE += ['csp.middleware.CSPMiddleware']
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
Rails:
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self
policy.style_src :self, :unsafe_inline
end
Start with a strict policy and use the Google CSP Evaluator to test it. If you load fonts, images, or scripts from CDNs, add them explicitly.
3. X-Frame-Options
What it does
Prevents your pages from being loaded inside an <iframe> on another website.
What attack it prevents
Clickjacking. An attacker loads your page in an invisible iframe, overlays their own UI on top, and tricks users into clicking buttons they can't see. "Click here to claim your prize" → the user actually clicks "Delete Account" or "Authorize Payment" on your site.
What happens without it
Any logged-in user is one click away from performing actions they didn't intend to. This is how a lot of social engineering attacks work.
How to add it
Nginx:
add_header X-Frame-Options "DENY" always;
Express / Node.js:
const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'deny' }));
Django:
# Enabled by default
X_FRAME_OPTIONS = 'DENY'
Rails:
# Enabled by default in modern Rails
# To override:
config.action_dispatch.default_headers['X-Frame-Options'] = 'DENY'
Use DENY unless you have a specific need to embed your site in iframes (in which case use SAMEORIGIN).
4. X-Content-Type-Options
What it does
Tells the browser to trust the Content-Type you declared and NOT try to "sniff" the actual content.
What attack it prevents
MIME-sniffing attacks. Without this header, an attacker uploads a file that looks like an image (evil.png) but contains JavaScript. When the browser fetches it, MIME sniffing kicks in, recognizes the JS, and executes it.
What happens without it
Any file upload feature becomes a script execution risk. Even just serving user content from your domain becomes dangerous.
How to add it
Nginx:
add_header X-Content-Type-Options "nosniff" always;
Express / Node.js:
const helmet = require('helmet');
app.use(helmet.noSniff());
Django:
# settings.py
SECURE_CONTENT_TYPE_NOSNIFF = True
Rails:
# Enabled by default in modern Rails
config.action_dispatch.default_headers['X-Content-Type-Options'] = 'nosniff'
This one is simple. There's no reason not to set it. Always use nosniff.
Bonus: Use Helmet (Node) or Django's SecurityMiddleware
If you're on Node, helmet adds all these headers and a few more in one line:
const helmet = require('helmet');
app.use(helmet());
If you're on Django, the built-in SecurityMiddleware does the same:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
# ... rest of middleware
]
Don't reinvent the wheel. Use the framework's built-in tools.
Verify it worked
After deploying, check your headers:
curl -I https://api.yourcompany.com
You should see all four headers in the response. Or run a free scan at GovernAPI — we'll check all four headers (plus a dozen other things) and tell you exactly what's missing.
TL;DR
| Header | Prevents | Set It To |
|--------|----------|-----------|
| Strict-Transport-Security | SSL stripping | max-age=31536000; includeSubDomains |
| Content-Security-Policy | XSS | default-src 'self' (then expand) |
| X-Frame-Options | Clickjacking | DENY |
| X-Content-Type-Options | MIME sniffing | nosniff |
Adding these four headers takes 5 minutes and prevents the most common API attacks. Do it today.
Scan your API for free
See your security score, vulnerabilities, and fix instructions in 60 seconds. No signup required.
Scan My API →