Security & Performance

Security Headers

20 min Lesson 8 of 35

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:

// Basic CSP Header
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;
Note: CSP directives:
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:

// CSP Source Keywords
'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'");
?>
Best Practice: Avoid '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:

// Report-Only Mode (test without blocking)
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:

// Three possible values
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
Warning: 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:

// Header Format
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
Note: MIME sniffing attacks occur when browsers ignore the Content-Type header and guess the file type based on content. This can allow execution of malicious files disguised as safe types.

Referrer-Policy

Controls how much referrer information is sent with requests:

// Referrer-Policy Values
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">
Best Practice: Use 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:

// Modern Permissions-Policy Syntax
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:

// Header Values
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
Warning: The X-XSS-Protection header is deprecated. It can actually introduce XSS vulnerabilities in some cases. Disable it and use Content-Security-Policy instead.

Complete Security Headers Configuration

A production-ready security headers setup:

// PHP - Complete Security Headers
<?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:

// Install Helmet
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:

// Command Line Testing
# 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');
?>
Exercise: Implement a complete security headers system:
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