Laravel · Practical guide
How to add security headers in Laravel
Add shared response headers in middleware, register it in the application pipeline and verify the response visitors receive. This guide uses Laravel 13 and its bootstrap/app.php structure. It starts with two explicit headers, then shows where application policies, proxy settings and public-response checks belong.
Add headers to the response, not the request
Suppose your Laravel page reaches the browser without X-Content-Type-Options or Referrer-Policy. If Laravel owns these headers, create a response middleware instead of adding the same settings to every controller. First check whether your web server or CDN already sends them; two independent owners can produce duplicate or conflicting values.
Run php artisan make:middleware SecurityHeaders --no-interaction and replace app/Http/Middleware/SecurityHeaders.php with the example below. It modifies the response after the downstream application has run. Setting a request header would not instruct the visitor’s browser. The Symfony response type also accommodates JSON, redirect and streamed response subclasses.
nosniff requires correct Content-Type values for scripts and styles. strict-origin-when-cross-origin limits referrer detail on cross-origin requests; evaluate no-referrer instead if your requirements prohibit sending any referrer. These two headers are a starting point, not a complete application security policy.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set(
'Referrer-Policy',
'strict-origin-when-cross-origin',
);
return $response;
}
} - Generate the middleware and add the response-header assignments above. Retain existing response content, cookies and cache settings.
- Register it in bootstrap/app.php as shown in the next section, then run the focused feature tests before deployment.
- Open a normal page and an application-generated 404 in DevTools > Network. Confirm both header values and check that scripts, styles and navigation still work.
Reference: Laravel 13: defining response middleware
Register it without replacing the existing stack
In a Laravel 13 application using Application::configure, merge this registration into the existing withMiddleware callback in bootstrap/app.php. Keep existing trusted-host, proxy and middleware configuration. The snippet is a fragment of that configuration chain; it is not a replacement bootstrap file.
append adds this middleware to the global stack, including requests that do not match a route. Registering only on the web group would leave other groups outside its coverage. If your upgraded application still uses an HTTP kernel to register middleware, follow that application structure and its installed version rather than adding a second registration mechanism.
Global registration does not mean every possible response passes through this code. An earlier middleware can return before it is reached, and an infrastructure error may never enter Laravel. Test the response types you depend on; do not replace Laravel’s default middleware stack just to make this class run.
// Fragment of the existing chain in bootstrap/app.php.
// Keep the existing import for:
// use Illuminate\Foundation\Configuration\Middleware;
->withMiddleware(function (Middleware $middleware): void {
// Preserve the other configuration already in this callback.
$middleware->append(\App\Http\Middleware\SecurityHeaders::class);
}) Reference: Laravel 13: global middleware registration
Give each policy an owner and a suitable scope
The example deliberately sets only two response headers. A CSP needs to match your scripts and integrations, while a session policy belongs in the component that creates the cookie. Adding every possible header to this middleware can break payments, embeds or authentication without improving the relevant protection.
Use the response header bag to set a header owned by this middleware. Do not replace the complete header bag or copy a generic Cache-Control value onto all responses: redirects, downloads and personal pages have their own requirements. If a controller needs a different referrer policy, define an explicit exception or separate middleware instead of relying on assignments that this class later overwrites.
| Policy | Where to configure it | What to verify |
|---|---|---|
| Content-Security-Policy | Application middleware when the policy depends on rendered HTML or per-response nonces. | The header and HTML use the same fresh nonce; required journeys work before enforcement. |
| Strict-Transport-Security | The HTTPS edge, or application code with trustworthy HTTPS detection. | HTTPS and renewal work for the intended scope; stage the lifetime before extending it. |
| Cookie attributes | Session configuration or the cookie API that issues each cookie. | Sign-in, callbacks and logout use the intended Secure, HttpOnly and SameSite attributes. |
| Framing and browser features | A policy tailored to the page’s embedding and feature requirements. | Required partner frames, camera access or sign-in popups are not accidentally blocked. |
Reference: Laravel 13: response headers and cookies
Find the layer that changes or omits a header
Behind a load balancer, Laravel may receive HTTP internally even when the visitor used HTTPS. If a policy depends on $request->isSecure(), configure trusted proxies and forwarded headers for your actual infrastructure. Do not trust arbitrary client-supplied forwarding headers just to make a check pass. Alternatively, let the HTTPS edge own HSTS consistently.
Laravel middleware cannot set headers on a static asset served directly by Nginx or a 502 produced before PHP responds. Compare a normal route, an application 404, a static file and an edge error in staging. If tests pass but the public response differs, inspect web-server directives, CDN transformations and cached older responses.
Use this macOS/Linux GET request against your own final public URL. Replace the example domain. Read the last response block after redirects and repeat for a known missing route. Check signed-in responses locally in your browser; HeaderScan does not authenticate or run JavaScript.
curl -q -sS -L --max-redirs 5 --max-time 30 \
-D - -o /dev/null 'https://example.com/'
curl -q -sS --max-time 30 -D - -o /dev/null \
'https://example.com/missing-security-check' Reference: Laravel 13: trusted proxies and forwarded headers
Test a normal route and a missing route
Create a PHPUnit feature test with php artisan make:test --phpunit SecurityHeadersTest --no-interaction, then use the example below. Replace / with a public route in your application and choose a missing URL that really returns 404. If your application uses domain routes, include its configured test host. Leave middleware enabled so the test exercises its registration as well as the values.
Run php artisan test --compact tests/Feature/SecurityHeadersTest.php. These tests cover Laravel’s response pipeline, not your production proxy or CDN. After deployment, repeat the public GET checks, inspect the final response in a browser and scan the same page with HeaderScan.
<?php
namespace Tests\Feature;
use Tests\TestCase;
class SecurityHeadersTest extends TestCase
{
public function test_public_page_sends_security_headers(): void
{
$this->get('/')
->assertOk()
->assertHeader('X-Content-Type-Options', 'nosniff')
->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
}
public function test_missing_page_sends_security_headers(): void
{
$this->get('/missing-security-check')
->assertNotFound()
->assertHeader('X-Content-Type-Options', 'nosniff')
->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
}
} - Confirm the focused tests pass with normal exception handling and middleware enabled.
- Check the deployed public response for exactly the intended values, including the application 404.
- Exercise login, forms and downloads; separately verify responses served directly by the web server or CDN.
Keep reading
Related guides
HTTP security headers: what to check first
Fix security header findings with CSP examples, a staged HSTS rollout, framing rules and practical browser and curl checks.
Read guide CachingCache-Control explained: no-cache, no-store and private
Choose Cache-Control for account pages, public HTML and versioned assets, then verify ETag revalidation and CDN behaviour with practical checks.
Read guide CookiesSecure, HttpOnly and SameSite cookies explained
Fix SameSite and session-cookie findings with Set-Cookie examples, host-only scope and browser checks for login, embedded content and logout.
Read guide Nginx configurationHow to configure security headers in Nginx
Add Nginx security headers to the correct HTTPS server, fix location inheritance and duplicates, then verify normal pages and error responses.
Read guide CSP implementationHow to roll out Content Security Policy with Report-Only
Build and test a CSP candidate, connect matching nonces and reporting endpoints, then enforce it with a practical rollback plan.
Read guideHTTP response headers reference: security, cookies and caching
Look up common HTTP response headers, their purpose and the checks to make before changing a policy.
Open reference