Security Headers for Web Applications

Philip Rehberger Feb 22, 2026 6 min read

Protect your web apps with security headers. Implement CSP, HSTS, and other headers to prevent common attacks.

Security Headers for Web Applications

Security headers instruct browsers how to handle your content, preventing common attacks like cross-site scripting, clickjacking, and data injection. While they're not a replacement for secure code, security headers provide an additional defense layer that mitigates vulnerabilities when they exist.

Implementing security headers is relatively low effort with high impact. A few HTTP headers can prevent entire classes of attacks. Understanding what each header does helps you configure them appropriately for your application.

Content Security Policy

Content Security Policy (CSP) controls which resources browsers can load. It's the most powerful security header, preventing XSS by restricting script sources.

// Middleware to add CSP header
class ContentSecurityPolicyMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        $csp = implode('; ', [
            "default-src 'self'",
            "script-src 'self' 'nonce-" . $this->getNonce() . "'",
            "style-src 'self' 'unsafe-inline'",
            "img-src 'self' data: https:",
            "font-src 'self'",
            "connect-src 'self' https://api.example.com",
            "frame-ancestors 'none'",
            "base-uri 'self'",
            "form-action 'self'",
        ]);

        $response->headers->set('Content-Security-Policy', $csp);

        return $response;
    }

    private function getNonce(): string
    {
        return app('csp-nonce');
    }
}

CSP directives control specific resource types. default-src sets the fallback for unspecified directives. script-src controls JavaScript sources. style-src controls stylesheets. connect-src controls fetch/XHR destinations.

Use nonces for inline scripts instead of unsafe-inline:

<script nonce="{{ app('csp-nonce') }}">
    // This script runs because nonce matches CSP
    initializeApp();
</script>

Start with a report-only policy to identify violations without breaking functionality:

$response->headers->set(
    'Content-Security-Policy-Report-Only',
    $csp . "; report-uri /csp-violations"
);

X-Frame-Options and frame-ancestors

X-Frame-Options prevents clickjacking by controlling whether the page can be embedded in frames. The CSP frame-ancestors directive provides more flexibility and should be preferred.

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

        // Legacy header for older browsers
        $response->headers->set('X-Frame-Options', 'DENY');

        // Modern CSP approach (also set in CSP header)
        // frame-ancestors 'none' = DENY
        // frame-ancestors 'self' = SAMEORIGIN
        // frame-ancestors https://trusted.com = specific origin

        return $response;
    }
}

Use DENY unless you specifically need framing. If embedding is required, specify exact allowed origins rather than SAMEORIGIN.

X-Content-Type-Options

X-Content-Type-Options prevents MIME-sniffing, where browsers guess content types and potentially execute malicious files as scripts.

$response->headers->set('X-Content-Type-Options', 'nosniff');

Always set this header. There's no legitimate reason to allow MIME-sniffing. Combined with proper Content-Type headers, it ensures browsers interpret content as intended.

Strict-Transport-Security

HTTP Strict Transport Security (HSTS) forces browsers to use HTTPS. After receiving an HSTS header, browsers automatically upgrade HTTP requests to HTTPS, preventing downgrade attacks.

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

        if ($request->secure()) {
            $response->headers->set(
                'Strict-Transport-Security',
                'max-age=31536000; includeSubDomains; preload'
            );
        }

        return $response;
    }
}

max-age specifies how long (in seconds) browsers should remember to use HTTPS. Start with a short duration (e.g., 3600) and increase once confident. includeSubDomains applies HSTS to all subdomains. preload enables inclusion in browser preload lists.

Be careful with HSTS. Once set with a long max-age, browsers will refuse HTTP connections until it expires. Ensure HTTPS works reliably before enabling.

Referrer-Policy

Referrer-Policy controls how much referrer information is sent with requests. The Referer header can leak sensitive URLs to third parties.

$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');

Common policies:

  • no-referrer: Never send referrer
  • same-origin: Send referrer only to same origin
  • strict-origin: Send origin only, no path
  • strict-origin-when-cross-origin: Full referrer to same origin, origin only to cross-origin (good default)

For sensitive applications, use no-referrer or same-origin to prevent any information leakage.

Permissions-Policy

Permissions-Policy (formerly Feature-Policy) controls browser features like geolocation, camera, and microphone. Restrict features you don't need to reduce attack surface.

$permissions = implode(', ', [
    'geolocation=()',           // Disable geolocation
    'camera=()',                // Disable camera
    'microphone=()',            // Disable microphone
    'payment=(self)',           // Allow payment API only for self
    'usb=()',                   // Disable USB access
    'accelerometer=()',         // Disable accelerometer
]);

$response->headers->set('Permissions-Policy', $permissions);

Disabling unnecessary features prevents their abuse even if an attacker injects code into your page.

X-XSS-Protection

X-XSS-Protection was a browser feature to detect and block reflected XSS. Modern browsers have deprecated it in favor of CSP, and it can introduce vulnerabilities in some cases.

// Disable the deprecated feature
$response->headers->set('X-XSS-Protection', '0');

Don't rely on X-XSS-Protection. Use CSP instead. Setting it to 0 prevents potential issues from the deprecated filter.

Implementation in Laravel

Create middleware that applies all security headers:

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

        // Prevent MIME-sniffing
        $response->headers->set('X-Content-Type-Options', 'nosniff');

        // Prevent framing (clickjacking)
        $response->headers->set('X-Frame-Options', 'DENY');

        // Disable deprecated XSS filter
        $response->headers->set('X-XSS-Protection', '0');

        // Control referrer information
        $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');

        // Restrict browser features
        $response->headers->set('Permissions-Policy', 'geolocation=(), camera=(), microphone=()');

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

        // CSP (configured per environment)
        if ($csp = config('security.csp')) {
            $response->headers->set('Content-Security-Policy', $csp);
        }

        return $response;
    }
}

Register the middleware in your kernel:

// app/Http/Kernel.php
protected $middlewareGroups = [
    'web' => [
        // ... other middleware
        \App\Http\Middleware\SecurityHeadersMiddleware::class,
    ],
];

Testing Security Headers

Verify headers are correctly set:

class SecurityHeadersTest extends TestCase
{
    /** @test */
    public function responses_include_security_headers(): void
    {
        $response = $this->get('/');

        $response->assertHeader('X-Content-Type-Options', 'nosniff');
        $response->assertHeader('X-Frame-Options', 'DENY');
        $response->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
    }

    /** @test */
    public function csp_header_is_present(): void
    {
        $response = $this->get('/');

        $csp = $response->headers->get('Content-Security-Policy');

        $this->assertStringContainsString("default-src 'self'", $csp);
        $this->assertStringContainsString("script-src", $csp);
    }
}

Use online tools like securityheaders.com to scan your production site for missing or misconfigured headers.

Common Pitfalls

CSP can break functionality if too restrictive. Test thoroughly and use report-only mode initially. Watch for inline scripts, inline styles, and third-party resources that need explicit allowing.

HSTS with long max-age can lock you out if HTTPS breaks. Start with short durations and increase gradually.

Different pages may need different CSP policies. Admin pages might allow different resources than public pages. Consider using CSP meta tags for page-specific policies.

@push('head')
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://admin-scripts.example.com">
@endpush

Conclusion

Security headers provide defense in depth against common web attacks. CSP prevents XSS by controlling resource loading. HSTS enforces HTTPS. X-Frame-Options and frame-ancestors prevent clickjacking. Referrer-Policy controls information leakage.

Implement security headers early and test them thoroughly. Start with report-only CSP to identify issues. Use automated testing to ensure headers remain correctly configured. Combined with secure coding practices, security headers significantly reduce your application's attack surface.

Share this article

Related Articles

Need help with your project?

Let's discuss how we can help you build reliable software.