What Are Security Headers
HTTP security headers are directives sent by the server in response headers that instruct the browser to enable specific security protections. Unlike code-level fixes, security headers provide defense-in-depth at the browser layer, mitigating entire vulnerability classes even when application code has flaws.
Security headers address the fundamental problem that browsers, by default, are permissive: they execute inline JavaScript, allow content embedding from any origin, accept mixed HTTP/HTTPS content, and trust MIME types declared by the server. These defaults prioritize compatibility over security. Security headers shift the browser to a secure-by-default posture.
The impact of missing security headers is measurable. In the 2023 OWASP Top 10, A05: Security Misconfiguration includes missing security headers as a primary risk factor. Research from Mozilla Observatory shows that over 70% of the top 1 million websites fail to implement even basic security headers like CSP or HSTS.
Security headers are mapped to multiple CWE categories: CWE-693 (Protection Mechanism Failure) and CWE-1021 (Improper Restriction of Rendered UI Layers or Frames). Regulatory frameworks including PCI-DSS, SOC 2, and HIPAA increasingly reference security header implementation as a baseline control.
Content-Security-Policy for XSS Prevention
Content-Security-Policy (CSP) is the most powerful security header for preventing Cross-Site Scripting (XSS) attacks. CSP defines an allowlist of sources from which the browser can load scripts, styles, images, and other resources. Any content from sources not in the allowlist is blocked, even if an attacker manages to inject HTML into the page.
How CSP Prevents XSS
XSS attacks inject malicious JavaScript into web pages. Traditional defenses (output encoding, sanitization) are applied at development time. CSP is runtime enforcement—even if encoding fails and an attacker injects <script>alert(document.cookie)</script>, CSP prevents execution if inline scripts are blocked.
CSP Directives Explained
- default-src: Fallback for all resource types not explicitly specified
- script-src: Allowed sources for JavaScript (most critical for XSS prevention)
- style-src: Allowed sources for CSS
- img-src: Allowed sources for images
- connect-src: Allowed sources for fetch, XHR, WebSocket
- frame-ancestors: Controls embedding (replaces X-Frame-Options)
- upgrade-insecure-requests: Automatically upgrades HTTP requests to HTTPS
Vulnerable (No CSP - Node.js/Express):
// VULNERABLE: No CSP header - all scripts allowed
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('<h1>Home</h1>');
});
// Any injected script will execute!
Secure (Strict CSP - Node.js/Express):
// SECURE: Strict CSP blocks inline scripts and unsafe-eval
const express = require('express');
const helmet = require('helmet');
const app = express();
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.trusted.com"],
styleSrc: ["'self'", "https://cdn.trusted.com"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
frameAncestors: ["'self'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: []
}
}));
app.get('/', (req, res) => {
res.send('<h1>Home</h1>');
});
Vulnerable (No CSP - Python/Flask):
# VULNERABLE: No CSP header
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return '<h1>Home</h1>'
# Inline scripts and eval() are allowed
Secure (Strict CSP - Python/Flask):
# SECURE: CSP via flask-talisman
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
csp = {
'default-src': ["'self'"],
'script-src': ["'self'", 'https://cdn.trusted.com'],
'style-src': ["'self'", 'https://cdn.trusted.com'],
'img-src': ["'self'", 'data:', 'https:'],
'connect-src': ["'self'"],
'font-src': ["'self'", 'https://fonts.gstatic.com'],
'object-src': ["'none'"],
'frame-ancestors': ["'self'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
'upgrade-insecure-requests': []
}
Talisman(app, content_security_policy=csp)
@app.route('/')
def home():
return '<h1>Home</h1>'
Vulnerable (No CSP - TypeScript/Next.js):
// VULNERABLE: No CSP in next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
// No security headers configured
};
export default config;
Secure (Strict CSP - TypeScript/Next.js):
// SECURE: CSP configured in next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' https://cdn.trusted.com",
"style-src 'self' https://cdn.trusted.com",
"img-src 'self' data: https:",
"connect-src 'self'",
"font-src 'self' https://fonts.gstatic.com",
"object-src 'none'",
"frame-ancestors 'self'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests"
].join('; ')
}
]
}
];
}
};
export default config;
Vulnerable (No CSP - Java/Spring Boot):
// VULNERABLE: No CSP header
@RestController
public class HomeController {
@GetMapping("/")
public String home() {
return "<h1>Home</h1>";
}
}
Secure (Strict CSP - Java/Spring Boot):
// SECURE: CSP via Spring Security
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers()
.contentSecurityPolicy("default-src 'self'; " +
"script-src 'self' https://cdn.trusted.com; " +
"style-src 'self' https://cdn.trusted.com; " +
"img-src 'self' data: https:; " +
"connect-src 'self'; " +
"font-src 'self' https://fonts.gstatic.com; " +
"object-src 'none'; " +
"frame-ancestors 'self'; " +
"base-uri 'self'; " +
"form-action 'self'; " +
"upgrade-insecure-requests");
}
}
Vulnerable (No CSP - Go):
// VULNERABLE: No CSP header
package main
import "net/http"
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("<h1>Home</h1>"))
})
http.ListenAndServe(":8080", nil)
}
Secure (Strict CSP - Go):
// SECURE: CSP middleware
package main
import (
"net/http"
"github.com/unrolled/secure"
)
func main() {
secureMiddleware := secure.New(secure.Options{
ContentSecurityPolicy: "default-src 'self'; " +
"script-src 'self' https://cdn.trusted.com; " +
"style-src 'self' https://cdn.trusted.com; " +
"img-src 'self' data: https:; " +
"connect-src 'self'; " +
"font-src 'self' https://fonts.gstatic.com; " +
"object-src 'none'; " +
"frame-ancestors 'self'; " +
"base-uri 'self'; " +
"form-action 'self'; " +
"upgrade-insecure-requests",
})
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("<h1>Home</h1>"))
})
http.ListenAndServe(":8080", secureMiddleware.Handler(mux))
}
CSP Best Practices
- Start with a strict policy (
default-src 'self') and relax only where necessary - Avoid
'unsafe-inline'and'unsafe-eval'—they defeat CSP's XSS protection - Use nonces or hashes for inline scripts if absolutely required
- Test CSP in report-only mode before enforcing (
Content-Security-Policy-Report-Only) - Set up CSP violation reporting to monitor blocked attacks
HSTS for HTTPS Enforcement
HTTP Strict Transport Security (HSTS) instructs browsers to only connect to the server via HTTPS, preventing protocol downgrade attacks and cookie hijacking. Without HSTS, the first connection to a site uses HTTP, which an attacker can intercept before the redirect to HTTPS.
How HSTS Works
When a server sends the HSTS header, the browser stores that directive and automatically upgrades all future HTTP requests to HTTPS for the specified duration (max-age). This prevents attacks like SSL stripping, where an attacker intercepts the initial HTTP request and prevents the upgrade to HTTPS.
HSTS Directives
- max-age: Duration in seconds the browser should enforce HTTPS (recommended: 31536000 = 1 year)
- includeSubDomains: Apply HSTS to all subdomains
- preload: Opt-in to the HSTS preload list (browsers enforce HSTS before the first visit)
Vulnerable (No HSTS - Node.js/Express):
// VULNERABLE: HTTPS configured but no HSTS header
const https = require('https');
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Home'));
https.createServer(options, app).listen(443);
// First connection vulnerable to SSL stripping
Secure (HSTS Enabled - Node.js/Express):
// SECURE: HSTS with preload and includeSubDomains
const https = require('https');
const express = require('express');
const helmet = require('helmet');
const app = express();
app.use(helmet.hsts({
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
}));
app.get('/', (req, res) => res.send('Home'));
https.createServer(options, app).listen(443);
Secure (HSTS - Python/Flask):
# SECURE: HSTS via flask-talisman
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
Talisman(app,
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000,
strict_transport_security_include_subdomains=True,
strict_transport_security_preload=True
)
@app.route('/')
def home():
return 'Home'
Secure (HSTS - Java/Spring Boot):
// SECURE: HSTS via Spring Security
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers()
.httpStrictTransportSecurity()
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
.preload(true);
}
Secure (HSTS - Go):
// SECURE: HSTS middleware
secureMiddleware := secure.New(secure.Options{
SSLRedirect: true,
STSSeconds: 31536000,
STSIncludeSubdomains: true,
STSPreload: true,
})
HSTS Best Practices
- Set
max-ageto 1 year (31536000 seconds) minimum - Use
includeSubDomainsto protect all subdomains - Submit to the HSTS preload list at hstspreload.org for first-visit protection
- Ensure all content (scripts, styles, images) is available over HTTPS before enabling HSTS
X-Frame-Options and Clickjacking Defense
X-Frame-Options prevents clickjacking attacks by controlling whether a page can be embedded in iframes. Clickjacking tricks users into clicking invisible UI elements overlaid on a legitimate page, often to perform actions without their knowledge (e.g., changing settings, transferring funds).
X-Frame-Options Values
- DENY: Page cannot be displayed in an iframe under any circumstances
- SAMEORIGIN: Page can only be displayed in an iframe on the same origin
- ALLOW-FROM uri: Deprecated—use CSP
frame-ancestorsinstead
Vulnerable (No X-Frame-Options - Node.js):
// VULNERABLE: Page can be embedded anywhere
app.get('/', (req, res) => {
res.send('<h1>Admin Panel</h1>');
});
// Attacker can iframe this page and overlay invisible UI
Secure (X-Frame-Options: DENY - Node.js):
// SECURE: Prevents all iframe embedding
app.use(helmet.frameguard({ action: 'deny' }));
app.get('/', (req, res) => {
res.send('<h1>Admin Panel</h1>');
});
Secure (X-Frame-Options: SAMEORIGIN - Python):
# SECURE: Allows iframe embedding only from same origin
from flask import Flask
app = Flask(__name__)
@app.after_request
def set_security_headers(response):
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
return response
Secure (X-Frame-Options - TypeScript/Next.js):
// SECURE: Set in next.config.ts
const config: NextConfig = {
async headers() {
return [{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' }
]
}];
}
};
Modern Alternative: CSP frame-ancestors
CSP's frame-ancestors directive is more flexible and is preferred over X-Frame-Options:
// CSP frame-ancestors (supports multiple origins)
Content-Security-Policy: frame-ancestors 'self' https://trusted.com;Additional Security Headers
X-Content-Type-Options
Prevents MIME type sniffing, where browsers execute files as JavaScript even when the server declares a different content type.
Vulnerable (No X-Content-Type-Options):
// VULNERABLE: Browser may execute as script despite text/plain
res.setHeader('Content-Type', 'text/plain');
res.send(userInput); // If userInput is "<script>...</script>", it may execute
Secure (X-Content-Type-Options: nosniff):
// SECURE: Prevents MIME sniffing
app.use(helmet.noSniff());
res.setHeader('Content-Type', 'text/plain');
res.send(userInput); // Will NOT execute as script
Referrer-Policy
Controls how much referrer information is sent with requests.
// SECURE: Strict referrer policy
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }));
Permissions-Policy (formerly Feature-Policy)
Controls which browser features and APIs can be used.
// SECURE: Disable camera, microphone, geolocation
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
next();
});Implementation in Express and Next.js
Complete Express.js Configuration
const express = require('express');
const helmet = require('helmet');
const app = express();
// Apply all security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.trusted.com"],
styleSrc: ["'self'", "https://cdn.trusted.com"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
frameAncestors: ["'self'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: []
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
frameguard: { action: 'deny' },
noSniff: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));
Complete Next.js Configuration
import type { NextConfig } from 'next';
const config: NextConfig = {
async headers() {
return [{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' https://cdn.trusted.com; style-src 'self' https://cdn.trusted.com; img-src 'self' data: https:; connect-src 'self'; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests"
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
}
]
}];
}
};
export default config;How CodeSlick Validates Security Headers
CodeSlick analyzes web application code across JavaScript, TypeScript, Python, Java, and Go to detect missing or misconfigured security headers:
- Missing CSP: Flags applications without Content-Security-Policy headers or with weak CSP (unsafe-inline, unsafe-eval)
- Missing HSTS: Detects HTTPS applications without Strict-Transport-Security headers or with insufficient max-age
- Missing X-Frame-Options: Identifies pages vulnerable to clickjacking attacks
- Missing X-Content-Type-Options: Flags applications without nosniff protection
- Weak Referrer-Policy: Detects referrer policies that leak sensitive URL parameters
All findings include CWE-693 and CWE-1021 classification, CVSS scoring, OWASP A05:2025 mapping, and framework-specific fix recommendations for Express.js, Next.js, Flask, Django, Spring Boot, and Go.
CodeSlick runs on every pull request via the GitHub App, on every commit via the CLI, and on-demand at codeslick.dev/analyze.
Scan your web application for missing security headers across JavaScript TypeScript Python Java and Go.