Companion Guide: This guide belongs to the Laravel kits. Every Laravel kit ships these checks built in. Explore Kit #1 (Filament), Kit #2 (Livewire), and Kit #3 (MongoDB).
Track 06 Lesson 20

Secure Headers

Curated by Umar Farooq • 13 min read
Quick Answer & Core Takeaway

HTTP security headers are directives sent by web servers to browsers, instructing them how to behave when handling web content. They provide an additional layer of defense against common web vulnerabilities including XSS, clickjacking, MIME sniffing attacks, and man-in-the-middle attacks.

Why HTTP Security Headers Matter

HTTP security headers are directives sent by web servers to browsers, instructing them how to behave when handling web content. They provide an additional layer of defense against common web vulnerabilities including XSS, clickjacking, MIME sniffing attacks, and man-in-the-middle attacks.

Impact of Missing Security Headers

X-Frame-Options Header

What is Clickjacking?

Clickjacking (UI redress attack) occurs when attackers trick users into clicking on invisible or disguised elements overlaid on legitimate pages. This can lead to:

X-Frame-Options Implementation

PHP / Laravel Code
<?php
// Prevent all framing
header('X-Frame-Options: DENY');

// Allow framing only from same origin
header('X-Frame-Options: SAMEORIGIN');

// Allow framing from specific origin
header('X-Frame-Options: ALLOW-FROM https://trusted-site.com');

Browser Support

Content Security Policy Frame Protection

PHP / Laravel Code
<?php
// More flexible frame control with CSP
header("Content-Security-Policy: frame-ancestors 'self'");

// Allow specific domains
header("Content-Security-Policy: frame-ancestors 'self' https://trusted.com");

X-Content-Type-Options Header

What is MIME Sniffing?

MIME sniffing occurs when browsers attempt to determine a file's MIME type by examining its content, rather than relying on the Content-Type header. This can lead to:

Implementation

PHP / Laravel Code
<?php
// Prevent MIME sniffing
header('X-Content-Type-Options: nosniff');

Example Attack Scenario

PHP / Laravel Code
<!-- Malicious image that contains HTML/JavaScript -->
<img src="evil.jpg" style="display:none">

<!-- Content of evil.jpg:
    <script>alert('MIME sniffing attack!')</script>
-->

<!-- Without X-Content-Type-Options: nosniff, browser might execute the script -->

Proper Content-Type Headers

[SECURE] Hardened Pattern
<?php
class SecureContentType
{
    private static array $contentTypes = [
        'html' => 'text/html; charset=utf-8',
        'json' => 'application/json; charset=utf-8',
        'xml' => 'application/xml; charset=utf-8',
        'css' => 'text/css; charset=utf-8',
        'js' => 'application/javascript; charset=utf-8',
        'png' => 'image/png',
        'jpg' => 'image/jpeg',
        'pdf' => 'application/pdf',
        'txt' => 'text/plain; charset=utf-8',
    ];

    public static function setContentType(string $type): void
    {
        if (isset(self::$contentTypes[$type])) {
            header('Content-Type: ' . self::$contentTypes[$type]);
            header('X-Content-Type-Options: nosniff');
        }
    }

    public static function serveFileSecurely(string $filePath, string $filename): void
    {
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

        if (isset(self::$contentTypes[$extension])) {
            header('Content-Type: ' . self::$contentTypes[$extension]);
            header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
            header('X-Content-Type-Options: nosniff');

            readfile($filePath);
            exit;
        }

        // Unknown file type - force download
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
        header('X-Content-Type-Options: nosniff');
    }
}

Content Security Policy (CSP)

What is CSP?

Content Security Policy is a security standard that helps prevent XSS attacks by controlling which resources can be loaded and executed. CSP provides fine-grained control over:

Basic CSP Implementation

PHP / Laravel Code
<?php
// Basic CSP - only allow same-origin resources
header("Content-Security-Policy: default-src 'self'");

Comprehensive CSP

[INSECURE] Vulnerable Pattern
<?php
class ContentSecurityPolicy
{
    private array $policies = [];

    public function __construct()
    {
        // Default restrictive policy
        $this->policies = [
            'default-src' => "'self'",
            'script-src' => "'self'",
            'style-src' => "'self'",
            'img-src' => "'self' data: https:",
            'font-src' => "'self' https://fonts.gstatic.com",
            'connect-src' => "'self'",
            'media-src' => "'self'",
            'object-src' => "'none'",
            'child-src' => "'self'",
            'frame-ancestors' => "'none'",
            'form-action' => "'self'",
        ];
    }

    public function allowInlineScripts(): self
    {
        $this->policies['script-src'] .= " 'unsafe-inline'";
        return $this;
    }

    public function allowInlineStyles(): self
    {
        $this->policies['style-src'] .= " 'unsafe-inline'";
        return $this;
    }

    public function allowExternalDomain(string $domain): self
    {
        $this->policies['default-src'] .= " {$domain}";
        return $this;
    }

    public function allowScriptDomain(string $domain): self
    {
        $this->policies['script-src'] .= " {$domain}";
        return $this;
    }

    public function addNonce(string $nonce): self
    {
        $this->policies['script-src'] = str_replace("'self'", "'self' 'nonce-{$nonce}'", $this->policies['script-src']);
        $this->policies['style-src'] = str_replace("'self'", "'self' 'nonce-{$nonce}'", $this->policies['style-src']);
        return $this;
    }

    public function enableUpgradeInsecureRequests(): self
    {
        $this->policies['upgrade-insecure-requests'] = '';
        return $this;
    }

    public function build(): string
    {
        $directives = [];

        foreach ($this->policies as $directive => $value) {
            if (!empty($value)) {
                $directives[] = "{$directive} {$value}";
            } elseif (isset($this->policies[$directive])) {
                // For directives without values like 'upgrade-insecure-requests'
                $directives[] = $directive;
            }
        }

        return implode('; ', $directives);
    }

    public static function create(): self
    {
        return new self();
    }
}

// Usage
$csp = ContentSecurityPolicy::create()
    ->allowInlineScripts()
    ->allowExternalDomain('https://cdn.example.com')
    ->enableUpgradeInsecureRequests()
    ->build();

header('Content-Security-Policy: ' . $csp);

CSP with Nonces for Inline Scripts

[SECURE] Hardened Pattern
<?php
class CSPNonceManager
{
    private static ?string $nonce = null;

    public static function generateNonce(): string
    {
        if (self::$nonce === null) {
            self::$nonce = bin2hex(random_bytes(16));
        }
        return self::$nonce;
    }

    public static function getCSPHeader(): string
    {
        $nonce = self::generateNonce();
        return "default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'nonce-{$nonce}'";
    }

    public static function createSecureScript(string $script): string
    {
        $nonce = self::generateNonce();
        return "<script nonce='{$nonce}'>{$script}</script>";
    }

    public static function createSecureStyle(string $style): string
    {
        $nonce = self::generateNonce();
        return "<style nonce='{$nonce}'>{$style}</style>";
    }
}

// Usage
header('Content-Security-Policy: ' . CSPNonceManager::getCSPHeader());

// Inline scripts are now allowed with nonce
echo CSPNonceManager::createSecureScript('console.log("Secure inline script");');

CSP Violation Reporting

[SECURE] Hardened Pattern
<?php
// [SECURE] Modern CSP violation reporting — replaces deprecated report-uri
// Step 1: Define the reporting endpoint
header('Reporting-Endpoints: csp-endpoint="https://yourdomain.com/api/csp-report"');

// Step 2: Reference the endpoint in CSP using report-to (NOT report-uri)
header("Content-Security-Policy: default-src 'self'; report-to csp-endpoint");

// Note: report-uri is DEPRECATED. Remove it from all CSP headers.
// Old (deprecated — do not use):
// header("Content-Security-Policy: default-src 'self'; report-uri /csp-report");

X-XSS-Protection Header — DEPRECATED [DEPRECATED]

Do NOT use X-XSS-Protection. This header has been removed from Chrome, Firefox, Edge, and Safari. It can introduce new XSS vulnerabilities in older browsers and is no longer recommended by OWASP.

The correct replacement is a strict Content-Security-Policy (see the CSP section above).

[INSECURE] Vulnerable Pattern
<?php
// [INSECURE] REMOVE THIS — Deprecated and harmful:
// header('X-XSS-Protection: 1; mode=block');

// [SECURE] USE THIS INSTEAD — Strict CSP with nonces:
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$nonce}'; object-src 'none'; base-uri 'self'");

Strict-Transport-Security (HSTS)

HTTPS Enforcement

HSTS forces browsers to use HTTPS connections, preventing protocol downgrade attacks.

PHP / Laravel Code
<?php
// Basic HSTS (6 months)
header('Strict-Transport-Security: max-age=15552000');

// Include subdomains
header('Strict-Transport-Security: max-age=15552000; includeSubDomains');

// Preload for HSTS preload list
header('Strict-Transport-Security: max-age=15552000; includeSubDomains; preload');

HSTS Preloading

HSTS preloading submits your domain to browser vendor lists for built-in HTTPS enforcement.

PHP / Laravel Code
<?php
class HSTSManager
{
    public static function setHSTSHeader(bool $includeSubdomains = true, bool $preload = false): void
    {
        $maxAge = 31536000; // 1 year

        $header = "max-age={$maxAge}";

        if ($includeSubdomains) {
            $header .= '; includeSubDomains';
        }

        if ($preload) {
            $header .= '; preload';
        }

        header("Strict-Transport-Security: {$header}");
    }

    public static function shouldEnableHSTS(): bool
    {
        // Only enable HSTS for HTTPS connections
        return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
    }

    public static function validateHSTSPreload(): array
    {
        $issues = [];

        // Check HTTPS redirect
        if (!self::shouldEnableHSTS()) {
            $issues[] = 'HSTS should only be enabled for HTTPS connections';
        }

        // Check for HTTP resources (would break HSTS)
        // This is a simplified check - implement more comprehensive validation

        return $issues;
    }
}

Referrer-Policy Header

Referrer Information Control

The Referrer-Policy header controls how much referrer information is sent with requests.

PHP / Laravel Code
<?php
// Strict origin - only send origin (scheme, host, port)
header('Referrer-Policy: strict-origin');

// When cross-origin, only send origin
header('Referrer-Policy: strict-origin-when-cross-origin');

// Never send referrer
header('Referrer-Policy: no-referrer');

// Default behavior (may leak sensitive information)
header('Referrer-Policy: unsafe-url');

Permissions Policy (Feature Policy)

Controlling Browser Features

Permissions Policy controls access to sensitive browser features.

PHP / Laravel Code
<?php
// Block camera and microphone access
header('Permissions-Policy: camera=(), microphone=()');

// Allow geolocation only for same origin
header('Permissions-Policy: geolocation=(self)');

// Block all payment APIs
header('Permissions-Policy: payment=()');

// Comprehensive policy
header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()');

Laravel Security Headers Implementation

Laravel Middleware for Security Headers

[INSECURE] Vulnerable Pattern
<?php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Security Headers Middleware — Laravel 13
 *
 * NOTE: X-XSS-Protection is intentionally REMOVED. It is deprecated and
 * removed from Chrome, Firefox, Edge, and Safari. Use CSP with nonces instead.
 *
 * Registration: bootstrap/app.php (Laravel 11+) — NOT Kernel.php (removed).
 */
class SecurityHeadersMiddleware
{
    private array $securityHeaders = [
        'X-Frame-Options'             => 'DENY',
        'X-Content-Type-Options'      => 'nosniff',
        // X-XSS-Protection intentionally omitted — DEPRECATED & REMOVED from modern browsers
        'Referrer-Policy'             => 'strict-origin-when-cross-origin',
        'Permissions-Policy'          => 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()',
        'Cross-Origin-Opener-Policy'  => 'same-origin',
        'Cross-Origin-Embedder-Policy'=> 'require-corp',
        'Cross-Origin-Resource-Policy'=> 'same-origin',
    ];

    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        // Remove server fingerprinting headers
        $response->headers->remove('X-Powered-By');
        $response->headers->remove('Server');

        // Apply core security headers
        foreach ($this->securityHeaders as $header => $value) {
            $response->headers->set($header, $value);
        }

        // Nonce-based CSP — no unsafe-inline
        $nonce = base64_encode(random_bytes(16));
        app()->instance('csp-nonce', $nonce);
        $csp = $this->buildContentSecurityPolicy($nonce);
        $response->headers->set('Content-Security-Policy', $csp);

        // Modern CSP reporting — Reporting-Endpoints (replaces deprecated Report-To)
        $response->headers->set(
            'Reporting-Endpoints',
            'csp-endpoint="' . config('app.url') . '/api/csp-report"'
        );

        // HSTS — only on HTTPS connections
        if ($request->secure()) {
            $response->headers->set(
                'Strict-Transport-Security',
                'max-age=31536000; includeSubDomains; preload'
            );
        }

        return $response;
    }

    private function buildContentSecurityPolicy(string $nonce): string
    {
        $policies = [
            "default-src 'self'",
            "script-src 'self' 'nonce-{$nonce}'",           // Nonce-based — no unsafe-inline
            "style-src 'self' 'nonce-{$nonce}'",            // Nonce-based — no unsafe-inline
            "img-src 'self' data: https:",
            "font-src 'self' https://fonts.gstatic.com",
            "connect-src 'self'",
            "media-src 'self'",
            "object-src 'none'",
            "frame-ancestors 'none'",                        // Replaces X-Frame-Options
            "form-action 'self'",
            "base-uri 'self'",
            "report-to csp-endpoint",                        // Modern reporting
        ];

        if (app()->environment('production', 'staging')) {
            $policies[] = "upgrade-insecure-requests";
        }

        return implode('; ', $policies);
    }
}

Registering Laravel Middleware (Laravel 11+)

Warning: app/Http/Kernel.php was removed in Laravel 11. Use bootstrap/app.php instead.

PHP / Laravel Code
<?php
// bootstrap/app.php — Laravel 11 / 12 / 13
use App\Http\Middleware\SecurityHeadersMiddleware;
use Illuminate\Foundation\Application;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
    )
    ->withMiddleware(function (\Illuminate\Foundation\Configuration\Middleware $middleware) {
        // Apply globally to all requests
        $middleware->append(SecurityHeadersMiddleware::class);

        // Or apply only to the web group
        // $middleware->web(append: [SecurityHeadersMiddleware::class]);
    })
    ->create();

Laravel CSP Package Usage

PHP / Laravel Code
<?php
// Using spatie/laravel-csp package
use Spatie\Csp\Directive;
use Spatie\Csp\Keyword;
use Spatie\Csp\Policy;

class CustomCspPolicy extends Policy
{
    public function configure()
    {
        $this
            ->add(Directive::DEFAULT, Keyword::SELF)
            ->add(Directive::SCRIPT, Keyword::SELF)
            ->add(Directive::SCRIPT, 'https://cdn.example.com')
            ->add(Directive::STYLE, Keyword::SELF)
            ->add(Directive::STYLE, Keyword::UNSAFE_INLINE)
            ->add(Directive::IMG, Keyword::SELF)
            ->add(Directive::IMG, 'data:')
            ->add(Directive::FONT, Keyword::SELF)
            ->add(Directive::CONNECT, Keyword::SELF);
    }
}

// In service provider
use Spatie\Csp\AddCspHeaders;

public function boot()
{
    app(AddCspHeaders::class)->addPolicy(new CustomCspPolicy());
}

Testing Security Headers

Manual Header Testing

PHP / Laravel Code
# Check security headers
curl -I https://yourdomain.com

# Expected output includes:
# X-Frame-Options: DENY
# X-Content-Type-Options: nosniff
# X-XSS-Protection: 1; mode=block
# Content-Security-Policy: default-src 'self'
# Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Automated Security Header Testing

PHP / Laravel Code
<?php
class SecurityHeadersTester
{
    private array $requiredHeaders = [
        'X-Frame-Options',
        'X-Content-Type-Options',
        'X-XSS-Protection',
        'Content-Security-Policy',
        'Strict-Transport-Security',
    ];

    private array $headerChecks = [
        'X-Frame-Options' => ['DENY', 'SAMEORIGIN'],
        'X-Content-Type-Options' => ['nosniff'],
        'X-XSS-Protection' => ['1; mode=block'],
        'Strict-Transport-Security' => null, // Just check presence for HTTPS
    ];

    public static function testHeaders(string $url): array
    {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, true);
        curl_setopt($ch, CURLOPT_NOBODY, true);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        $headers = self::parseHeaders($response);

        return self::analyzeHeaders($headers, $httpCode);
    }

    private static function parseHeaders(string $response): array
    {
        $headers = [];
        $lines = explode("\n", $response);

        foreach ($lines as $line) {
            if (strpos($line, ':') !== false) {
                [$name, $value] = explode(':', $line, 2);
                $headers[trim($name)] = trim($value);
            }
        }

        return $headers;
    }

    private static function analyzeHeaders(array $headers, int $httpCode): array
    {
        $results = [
            'score' => 0,
            'max_score' => 5,
            'issues' => [],
            'passed' => [],
        ];

        $tester = new self();

        // Check required headers
        foreach ($tester->requiredHeaders as $header) {
            if (!isset($headers[$header])) {
                $results['issues'][] = "Missing {$header} header";
            } else {
                $results['passed'][] = "{$header}: {$headers[$header]}";
                $results['score']++;
            }
        }

        // Validate header values
        foreach ($tester->headerChecks as $header => $expectedValues) {
            if (isset($headers[$header]) && $expectedValues !== null) {
                $value = $headers[$header];
                $valid = false;

                foreach ($expectedValues as $expected) {
                    if (stripos($value, $expected) !== false) {
                        $valid = true;
                        break;
                    }
                }

                if (!$valid) {
                    $results['issues'][] = "Invalid {$header} value: {$value}";
                    $results['score']--;
                }
            }
        }

        // Special check for HSTS on HTTPS
        if ($httpCode >= 200 && $httpCode < 400) {
            $url = 'https://' . $_SERVER['HTTP_HOST'] ?? 'localhost';
            if (strpos($url, 'https://') === 0) {
                if (!isset($headers['Strict-Transport-Security'])) {
                    $results['issues'][] = 'Missing HSTS header for HTTPS site';
                    $results['score']--;
                }
            }
        }

        return $results;
    }
}

// Usage
$results = SecurityHeadersTester::testHeaders('https://yourdomain.com');
echo "Security Headers Score: {$results['score']}/{$results['max_score']}\n";

if (!empty($results['issues'])) {
    echo "Issues found:\n";
    foreach ($results['issues'] as $issue) {
        echo "- {$issue}\n";
    }
}

Security Headers Checklist

Essential Headers

Additional Security Headers (2026 Standard)

Implementation Steps

Summary: Security Headers Implementation

  1. Implement X-Frame-Options - Prevent clickjacking attacks
  2. Add X-Content-Type-Options - Prevent MIME sniffing
  3. Enable Content Security Policy - Control resource execution
  4. Use Strict-Transport-Security - Enforce HTTPS connections
  5. Set Referrer-Policy - Control referrer information leakage
  6. Implement Permissions Policy - Control browser feature access
  7. Test thoroughly - Use automated tools and manual testing
  8. Monitor violations - Set up CSP reporting endpoints
  9. Keep updated - Regularly review and update policies
  10. Use framework features - Leverage Laravel's built-in security

Next Steps

Now that you understand security headers, explore:

Remember: Security headers provide defense-in-depth protection. Implement them consistently across all pages and API endpoints!

AI Assistant Shortcuts
Ask AI about this topic
← Prev: Secure Configuration & Environment
Next: Secure Server Deployment & TLS 1.3 →
UF

Curated by Umar Farooq

Senior Full-Stack & Security Engineer with 5+ years of production experience in Laravel, PHP, and modern web application security. Author of the Laravel AI Security Masterclass.