Security Headers: Browser-Enforced Policies
Security Headers: Telling the Browser How to Protect Your Application
Security headers are HTTP response headers that instruct the browser to enforce specific security policies. They provide a second layer of defense — even if an application has an XSS or clickjacking vulnerability, the browser may block the attack based on the header policies.
Content Security Policy (CSP)
CSP is the most powerful security header. It restricts which resources (scripts, styles, images, fonts, connections) the browser will load and execute for a given page. CSP is the most effective defense-in-depth against XSS.
Basic CSP:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
This policy:
default-src 'self': Only load resources from the same origin by defaultscript-src 'self': Only execute scripts from the same origin (blocks all inline scripts and external scripts)style-src 'self': Only load stylesheets from the same originimg-src 'self': Only load images from the same originconnect-src 'self': Only make AJAX/fetch requests to the same originframe-ancestors 'none': Prevent the page from being embedded in frames (clickjacking protection)base-uri 'self': Restrict the base URL for relative linksform-action 'self': Only submit forms to the same origin
CSP Directives:
| Directive | Controls |
|---|---|
default-src |
Fallback for all resource types |
script-src |
JavaScript sources |
style-src |
CSS sources |
img-src |
Image sources |
font-src |
Font sources |
connect-src |
AJAX, WebSocket, EventSource targets |
frame-src |
Sources for nested frames |
frame-ancestors |
Which pages can embed this page |
base-uri |
Base URL for relative links |
form-action |
Valid form submission targets |
Nonce-Based CSP:
To allow specific inline scripts while blocking all others, use a nonce (number used once):
Content-Security-Policy: script-src 'nonce-abc123'
<script nonce="abc123">
// This script is allowed because it has the correct nonce
console.log('trusted inline script');
</script>
<script>
// This script is blocked — no nonce
alert('injected script');
</script>
The nonce must be cryptographically random and regenerated on every request.
CSP Reporting:
CSP can report violations to a specified endpoint without blocking (report-only mode):
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /api/csp-report
This is useful for testing a CSP policy before enforcing it.
X-Frame-Options and frame-ancestors
Clickjacking is an attack where an attacker embeds a vulnerable page in a transparent iframe and tricks the user into clicking elements they cannot see.
X-Frame-Options:
X-Frame-Options: DENY
DENY: The page cannot be embedded in any frameSAMEORIGIN: The page can only be embedded in frames from the same origin
frame-ancestors (CSP, modern replacement):
Content-Security-Policy: frame-ancestors 'none'
'none': No page can embed this page'self': Only same-origin pages can embedhttps://example.com: Only the specified origin can embed
frame-ancestors is more flexible than X-Frame-Options and supports multiple origins. Use both for backward compatibility.
HTTP Strict Transport Security (HSTS)
HSTS forces the browser to upgrade every future request to HTTPS for a specified duration, preventing SSL stripping attacks:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age=31536000: Remember the policy for one year (in seconds)includeSubDomains: Apply to all subdomainspreload: Include in the browser's built-in HSTS list (requires submission to hstspreload.org)
Why HSTS matters: Without HSTS, an attacker on the same network can intercept the initial HTTP request and prevent the upgrade to HTTPS (SSL stripping). With HSTS, the browser automatically upgrades to HTTPS before any network request is made.
Other Important Security Headers
X-Content-Type-Options:
X-Content-Type-Options: nosniff
Prevents MIME type sniffing — the browser will not guess the content type, blocking attacks that rely on the browser interpreting a file as a different type.
Referrer-Policy:
Referrer-Policy: strict-origin-when-cross-origin
Controls how much referrer information is included with requests. strict-origin-when-cross-origin sends the full URL for same-origin requests, only the origin for cross-origin requests, and no referrer for HTTPS-to-HTTP requests.
Permissions-Policy:
Permissions-Policy: camera=(), microphone=(), geolocation=()
Restricts which browser features (camera, microphone, geolocation) the page can access.
Implementing Security Headers in Practice
Configuring Security Headers Across the Stack
Security headers can be set at multiple levels: the web server, the application framework, or a reverse proxy. The best practice is to set them at the outermost layer (reverse proxy or CDN) so they apply to all responses, including static files and error pages.
Setting Headers in Express.js
const express = require('express');
const helmet = require('helmet');
const app = express();
// Helmet sets multiple security headers by default
app.use(helmet());
// Customize CSP
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
styleSrc: ["'self'", "'unsafe-inline'"], // unsafe-inline for styles is generally acceptable
imgSrc: ["'self'", 'data:'],
connectSrc: ["'self'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"]
}
}));
// HSTS
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true
}));
// Generate nonce for each request
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
Setting Headers in Nginx
server {
listen 443 ssl;
server_name example.com;
# CSP
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Clickjacking protection
add_header X-Frame-Options "DENY" always;
# MIME sniffing protection
add_header X-Content-Type-Options "nosniff" always;
# Referrer policy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Remove server version header
server_tokens off;
}
Setting Headers in Flask
from flask import Flask, render_template_string
from flask_talisman import Talisman
app = Flask(__name__)
# Talisman sets security headers
Talisman(app,
content_security_policy={
'default-src': "'self'",
'script-src': "'self'",
'style-src': ["'self'", "'unsafe-inline'"],
'img-src': ["'self'", 'data:'],
'frame-ancestors': "'none'",
},
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000,
strict_transport_security_include_subdomains=True,
x_frame_options='DENY',
x_content_type_options='nosniff',
)
Testing Security Headers
Use these tools to verify your security headers:
# Check headers with curl
curl -I https://example.com
# Online scanner
# https://securityheaders.com/
# Command-line tool
npx lighthouse https://example.com --view
Practice: Write a CSP Policy
Write a CSP policy for a web application that:
- Loads scripts only from its own CDN (cdn.example.com)
- Allows inline styles but not inline scripts
- Loads images from its own domain and data URIs
- Prevents the page from being embedded in frames
- Makes API calls only to api.example.com
Practice Problems
Your CSP report-only mode receives the following violation: ```json { "csp-report": { "document-uri": "https://example.com/dashboard", "violated-directive": "script-src", "blocked-uri": "https://evil.com/inject.js", "effective-directive": "script-src", "original-policy": "default-src 'self'; script-src 'self'" } } ``` Explain what happened and whether this indicates an attack or a legitimate issue.
Solution
The violation indicates that the browser blocked a script load from `https://evil.com/inject.js` because the CSP policy only allows scripts from `'self'` (same origin). This could indicate:
1. **An XSS attack**: An attacker successfully injected a `<script src="https://evil.com/inject.js">` tag into the page, and CSP blocked it. This is CSP working as intended.
2. **A legitimate third-party script**: The application may have started using a third-party script without updating the CSP. Check with the development team.
3. **A browser extension**: Some browser extensions inject scripts that trigger CSP violations.
Investigate by checking the `document-uri` (which page), the `blocked-uri` (what was blocked), and correlating with server logs for that page. If it is an attack, the CSP prevented the XSS from executing. Configure Nginx to set the following security headers for all responses: 1. CSP allowing scripts from self and cdn.example.com, styles from self with unsafe-inline, images from self and data: 2. HSTS with 1-year max-age, including subdomains 3. X-Frame-Options: DENY 4. X-Content-Type-Options: nosniff 5. Remove the Server header version information
Solution
```nginx
server {
listen 443 ssl;
server_name example.com;
# CSP
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'" always;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Clickjacking protection
add_header X-Frame-Options "DENY" always;
# MIME sniffing protection
add_header X-Content-Type-Options "nosniff" always;
# Remove server version
server_tokens off;
# Additional headers
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
```
The `always` parameter ensures the headers are added to all responses, including error pages (404, 500, etc.). Quiz
1. What does the CSP directive `script-src 'self'` accomplish?
2. What is the purpose of the `frame-ancestors 'none'` directive in CSP?
3. What does HSTS (HTTP Strict Transport Security) protect against?
4. What is the purpose of the `X-Content-Type-Options: nosniff` header?
5. Why is `Content-Security-Policy-Report-Only` useful during CSP deployment?
Flashcards
Question
What does `script-src 'self'` in CSP accomplish?
Click to reveal answer
Answer
It restricts script execution to the same origin only, blocking all inline scripts and external scripts. Even if an attacker injects a script, the browser refuses to execute it.
Question
What is the purpose of `frame-ancestors 'none'`?
Click to reveal answer
Answer
It prevents the page from being embedded in any iframe by any page, blocking clickjacking attacks. It is the CSP equivalent of X-Frame-Options: DENY.
Question
What does HSTS protect against?
Click to reveal answer
Answer
SSL stripping attacks — it forces the browser to upgrade every request to HTTPS before any network request is made, preventing attackers from intercepting the initial HTTP request.
Question
What is CSP report-only mode?
Click to reveal answer
Answer
A testing mode where the browser sends violation reports to a specified endpoint without blocking any resources. Used to identify legitimate resources that need to be added to the policy before switching to enforcing mode.
Question
What does `X-Content-Type-Options: nosniff` prevent?
Click to reveal answer
Answer
MIME type sniffing — the browser will not guess the content type and will respect the server's declared content type. This blocks attacks that rely on the browser interpreting a file as a different type (e.g., executing an uploaded file as a script).
Revision Notes
Key Takeaways
- 1. CSP restricts which resources the browser will load and execute — the most effective defense-in-depth against XSS
- 2. X-Frame-Options and frame-ancestors block clickjacking by preventing the page from being embedded in frames
- 3. HSTS prevents SSL stripping by forcing the browser to upgrade to HTTPS before any network request
- 4. Use CSP report-only mode to test policies before enforcing them
- 5. Set security headers at the outermost layer (reverse proxy/CDN) so they apply to all responses
Interview Tips
- • Explain how CSP works with specific examples of directives and what each one controls
- • Discuss the difference between X-Frame-Options and frame-ancestors, and why you would use both
- • Describe how to deploy CSP safely using report-only mode to avoid breaking legitimate functionality
- • Connect security headers to the OWASP Top 10 — they are defense-in-depth for XSS (A03) and clickjacking
Cheat Sheet
Security Headers and CSP Cheat Sheet
Phase: Application Security and Secure Coding | Level: intermediate
Essential Headers:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
CSP Directives:
script-src— JavaScript sources (blocks inline scripts and event handlers)style-src— CSS sourcesimg-src— Image sourcesconnect-src— AJAX/WebSocket targetsframe-ancestors— Which pages can embed this page (clickjacking)base-uri— Base URL for relative linksform-action— Valid form submission targets
Key points:
- Content Security Policy restricts which script sources the browser will execute
- X-Frame-Options and frame-ancestors block cross-site framing used in clickjacking
- HSTS forces browsers to upgrade every future request to HTTPS for a set duration
- Use CSP report-only mode to test before enforcing
Tags: web-security, headers
Continue Learning
Before You Start
- No prerequisites listed.