Security Headers
Security Headers
HTTP security headers provide defense-in-depth protection against common web vulnerabilities. Properly configured headers prevent XSS, clickjacking, MIME sniffing, and other attacks at the browser level.
Content-Security-Policy (CSP)
CSP is the most powerful security header, preventing XSS attacks by controlling which resources can be loaded:
Content-Security-Policy: default-src 'self'
// Detailed CSP Configuration
<?php
header(
"Content-Security-Policy: " .
"default-src 'self'; " . // Default: same origin
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " . // Scripts
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " . // Styles
"img-src 'self' data: https:; " . // Images
"font-src 'self' https://fonts.gstatic.com; " . // Fonts
"connect-src 'self' https://api.example.com; " . // AJAX/WebSocket
"frame-src 'none'; " . // Iframes
"object-src 'none'; " . // Flash/plugins
"base-uri 'self'; " . // <base> tag
"form-action 'self'; " . // Form submissions
"frame-ancestors 'none'; " . // Embedding protection
"upgrade-insecure-requests" // HTTP → HTTPS
);
?>
// Apache Configuration
Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'"
// Nginx Configuration
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'" always;
default-src: Fallback for all fetch directives
script-src: JavaScript sources
style-src: CSS sources
img-src: Image sources
font-src: Font sources
connect-src: AJAX, WebSocket, EventSource
frame-src: Iframe sources
object-src: Flash, Java applets
CSP Source Keywords
CSP uses special keywords to define allowed sources:
'none' // Block all sources
'self' // Same origin (protocol + domain + port)
'unsafe-inline' // Allow inline scripts/styles (NOT RECOMMENDED)
'unsafe-eval' // Allow eval() and similar (NOT RECOMMENDED)
data: // Allow data: URIs (base64 images)
https: // Allow any HTTPS URL
*.example.com // Allow subdomains
// Nonce-based CSP (RECOMMENDED for inline scripts)
<?php
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: script-src 'self' 'nonce-$nonce'");
?>
<!-- Use nonce in script tags -->
<script nonce="<?php echo $nonce; ?>">
// Inline JavaScript allowed with this nonce
console.log('Secure inline script');
</script>
// Hash-based CSP (for static inline scripts)
<?php
$script = "console.log('hello');";
$hash = base64_encode(hash('sha256', $script, true));
header("Content-Security-Policy: script-src 'self' 'sha256-$hash'");
?>
'unsafe-inline' and 'unsafe-eval'. Use nonces for dynamic content or hashes for static content. This provides maximum XSS protection.CSP Reporting
CSP can report violations to help identify issues before enforcing strict policies:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
// Enforcement Mode with Reporting
Content-Security-Policy: default-src 'self'; report-uri /csp-report
// PHP Report Handler
<?php
// /csp-report endpoint
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$report = json_decode(file_get_contents('php://input'), true);
error_log(sprintf(
"CSP Violation: %s blocked %s on %s",
$report['csp-report']['violated-directive'] ?? 'unknown',
$report['csp-report']['blocked-uri'] ?? 'unknown',
$report['csp-report']['document-uri'] ?? 'unknown'
));
// Store in database for analysis
logCspViolation($report);
http_response_code(204);
}
?>
// Report-To API (modern replacement for report-uri)
Content-Security-Policy: default-src 'self'; report-to csp-endpoint
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"/csp-report"}]}
X-Frame-Options
Prevents clickjacking attacks by controlling whether your site can be embedded in iframes:
X-Frame-Options: DENY // Never allow framing
X-Frame-Options: SAMEORIGIN // Allow same-origin framing only
X-Frame-Options: ALLOW-FROM https://example.com // Deprecated
// PHP Implementation
<?php
header('X-Frame-Options: DENY');
// Or
header('X-Frame-Options: SAMEORIGIN');
?>
// Apache Configuration
Header always set X-Frame-Options "SAMEORIGIN"
// Nginx Configuration
add_header X-Frame-Options "SAMEORIGIN" always;
// Modern CSP Alternative (more flexible)
Content-Security-Policy: frame-ancestors 'self'
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self' https://trusted.com
ALLOW-FROM is deprecated and not supported by most browsers. Use CSP frame-ancestors directive instead for multi-domain iframe control.X-Content-Type-Options
Prevents MIME type sniffing, forcing browsers to respect declared Content-Type:
X-Content-Type-Options: nosniff
// PHP Implementation
<?php
header('X-Content-Type-Options: nosniff');
?>
// Apache Configuration
Header always set X-Content-Type-Options "nosniff"
// Nginx Configuration
add_header X-Content-Type-Options "nosniff" always;
// Why it matters:
// WITHOUT nosniff: Browser may execute text/plain as JavaScript
<script src="user-uploaded-file.txt"></script> // Could execute!
// WITH nosniff: Browser respects Content-Type
// text/plain file will NOT execute as JavaScript
Referrer-Policy
Controls how much referrer information is sent with requests:
no-referrer // Never send referrer
no-referrer-when-downgrade // Default: send unless HTTPS → HTTP
origin // Send origin only (https://example.com)
origin-when-cross-origin // Full URL for same-origin, origin for cross-origin
same-origin // Send referrer to same origin only
strict-origin // Send origin unless HTTPS → HTTP
strict-origin-when-cross-origin // Recommended: Full URL same-origin, origin cross-origin
unsafe-url // Always send full URL (NOT RECOMMENDED)
// PHP Implementation
<?php
header('Referrer-Policy: strict-origin-when-cross-origin');
?>
// Apache Configuration
Header always set Referrer-Policy "strict-origin-when-cross-origin"
// Nginx Configuration
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
// HTML Meta Tag
<meta name="referrer" content="strict-origin-when-cross-origin">
strict-origin-when-cross-origin as a balanced policy. It protects user privacy while providing useful analytics data for same-origin navigation.Permissions-Policy (formerly Feature-Policy)
Controls which browser features and APIs can be used:
Permissions-Policy: geolocation=(), camera=(), microphone=()
// Detailed Configuration
<?php
header(
"Permissions-Policy: " .
"geolocation=(), " . // Block geolocation
"camera=(), " . // Block camera
"microphone=(), " . // Block microphone
"payment=(self), " . // Allow payment APIs for same origin
"usb=(), " . // Block USB access
"accelerometer=(), " . // Block accelerometer
"gyroscope=(), " . // Block gyroscope
"magnetometer=(), " . // Block magnetometer
"fullscreen=(self)" // Allow fullscreen for same origin
);
?>
// Allow specific origins
Permissions-Policy: camera=(self "https://trusted-video.com")
// Legacy Feature-Policy (deprecated but still supported)
Feature-Policy: geolocation 'none'; camera 'none'; microphone 'none'
// Apache Configuration
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
// Nginx Configuration
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
X-XSS-Protection (Legacy)
Legacy XSS filter control. Modern sites should rely on CSP instead:
X-XSS-Protection: 0 // Disable XSS filter
X-XSS-Protection: 1 // Enable XSS filter
X-XSS-Protection: 1; mode=block // Enable and block page
// PHP Implementation
<?php
// Recommended: Disable (conflicts with CSP)
header('X-XSS-Protection: 0');
?>
// Apache Configuration
Header always set X-XSS-Protection "0"
// Why disable?
// - Modern browsers deprecated this feature
// - Can introduce vulnerabilities
// - CSP provides better protection
// - Creates false sense of security
Complete Security Headers Configuration
A production-ready security headers setup:
<?php
// Force HTTPS
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
// Content Security Policy
$nonce = base64_encode(random_bytes(16));
header(
"Content-Security-Policy: " .
"default-src 'self'; " .
"script-src 'self' 'nonce-$nonce' https://cdn.jsdelivr.net; " .
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " .
"img-src 'self' data: https:; " .
"font-src 'self' https://fonts.gstatic.com; " .
"connect-src 'self'; " .
"frame-src 'none'; " .
"object-src 'none'; " .
"base-uri 'self'; " .
"form-action 'self'; " .
"frame-ancestors 'none'; " .
"upgrade-insecure-requests; " .
"block-all-mixed-content"
);
// Clickjacking protection
header('X-Frame-Options: DENY');
// Prevent MIME sniffing
header('X-Content-Type-Options: nosniff');
// Referrer policy
header('Referrer-Policy: strict-origin-when-cross-origin');
// Permissions policy
header('Permissions-Policy: geolocation=(), camera=(), microphone=()');
// Disable XSS filter (deprecated)
header('X-XSS-Protection: 0');
?>
// Apache - Complete Configuration
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
Header always set X-XSS-Protection "0"
</IfModule>
// Nginx - Complete Configuration
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'" 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=(), camera=(), microphone=()" always;
add_header X-XSS-Protection "0" always;
Helmet.js for Node.js/Express
Helmet.js automatically sets security headers for Node.js applications:
npm install helmet
// Basic Implementation
const express = require('express');
const helmet = require('helmet');
const app = express();
// Use all default headers
app.use(helmet());
// Custom Configuration
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "cdn.jsdelivr.net"],
styleSrc: ["'self'", "'unsafe-inline'", "fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "fonts.gstatic.com"],
connectSrc: ["'self'"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
frameguard: {
action: 'deny',
},
referrerPolicy: {
policy: 'strict-origin-when-cross-origin',
},
})
);
// Individual Headers
app.use(helmet.contentSecurityPolicy({ /* options */ }));
app.use(helmet.hsts({ /* options */ }));
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.noSniff());
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }));
Testing and Auditing Security Headers
Tools and techniques to verify security headers are correctly configured:
# Check headers with curl
curl -I https://example.com
# Specific header check
curl -I https://example.com | grep "Content-Security-Policy"
// Browser DevTools
// 1. Open DevTools (F12)
// 2. Network tab
// 3. Reload page
// 4. Click request
// 5. View "Headers" section
// Online Security Header Scanners
// 1. https://securityheaders.com/
// - Grades A+ to F
// - Detailed recommendations
// 2. https://observatory.mozilla.org/
// - Comprehensive security scan
// - CSP validator
// 3. https://csp-evaluator.withgoogle.com/
// - CSP policy analyzer
// - Identifies weaknesses
// PHP Header Testing Script
<?php
function checkSecurityHeaders($url) {
$headers = get_headers($url, 1);
$requiredHeaders = [
'Strict-Transport-Security',
'Content-Security-Policy',
'X-Frame-Options',
'X-Content-Type-Options',
'Referrer-Policy'
];
foreach ($requiredHeaders as $header) {
if (isset($headers[$header])) {
echo "✓ $header: {$headers[$header]}\n";
} else {
echo "✗ $header: MISSING\n";
}
}
}
checkSecurityHeaders('https://example.com');
?>
1. Create a PHP script that sets all recommended security headers
2. Implement nonce-based CSP for inline scripts
3. Configure HSTS with 1-year max-age and preload
4. Set up CSP reporting endpoint to log violations
5. Configure Referrer-Policy and Permissions-Policy
6. Test headers at securityheaders.com (aim for A+ grade)
7. Add security headers to Apache or Nginx configuration
8. Verify headers using browser DevTools
9. Test CSP by attempting to load blocked resources
10. Monitor CSP violation reports for 1 week to identify issues