Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

The Security Headers Most Sites Forget (and How to Add Them in Laravel)

About Post

You can spend weeks on authentication, validation and rate limiting, and still ship a site that any other website can load in an invisible iframe and trick your users into clicking.

The fix for that, and for a few other nasty attacks, is not a library or a rewrite. It's a handful of HTTP response headers. They take an hour to add, they cost nothing at runtime, and a surprising number of production sites still don't send them.

Here are the mistakes I see most often, why each header matters, and a Laravel middleware that covers the basics at the end.

Mistake 1: no Content-Security-Policy at all

What it protects: Content-Security-Policy (CSP) tells the browser which sources of scripts, styles, images and frames are allowed. If an attacker manages to inject a <script> tag through a comment field or a forgotten unescaped output, a good CSP stops the browser from running it. It's your second line of defence against XSS, for the day the first one fails.

Why people skip it: because a strict CSP breaks things. Inline scripts, analytics snippets, a chat widget, a font from a CDN: all blocked until you allow them. So teams either skip CSP or add one that allows everything, including 'unsafe-inline' scripts, which removes most of the protection.

The fix: roll it out in two steps.

  1. Start with Content-Security-Policy-Report-Only. The browser enforces nothing, but reports every violation in the console (and to a reporting endpoint if you set one). Now you can see what would break.
  2. Tighten the policy until the reports are clean, then switch to the real Content-Security-Policy header.

For inline scripts you really need, use nonces instead of 'unsafe-inline'. Laravel's Vite integration can generate a nonce for its script tags with Vite::useCspNonce(), which you can then add to your policy.

Mistake 2: relying only on X-Frame-Options

What it protects: clickjacking. An attacker's page loads your site in a transparent iframe and lines up your "Confirm payment" button under their "Claim prize" button. The user thinks they clicked one thing and clicked the other.

The mistake: X-Frame-Options was the original fix, and it still works for DENY and SAMEORIGIN. But its ALLOW-FROM option is obsolete and ignored by modern browsers, so it can't express "only my own partner domain may frame this".

The fix: use the CSP frame-ancestors directive, which replaces it and supports a list of allowed origins: frame-ancestors 'self' https://partner.example.com. Sending X-Frame-Options: SAMEORIGIN as well doesn't hurt and covers very old browsers.

Mistake 3: HTTPS without HSTS

What it protects: you redirect HTTP to HTTPS, so you're safe, right? Not quite. The very first request a user types as example.com goes out over plain HTTP before your redirect happens, and on a hostile network that first request can be intercepted. Strict-Transport-Security tells the browser to use HTTPS for your domain from now on, without ever trying HTTP first.

The mistakes: adding includeSubDomains before checking that every subdomain supports HTTPS (that old reports. subdomain on an HTTP-only server will stop working), and jumping straight to preload, which is hard to undo once browsers ship your domain in their preload list.

The fix: start with a short max-age, confirm nothing breaks, then raise it to a year. Add includeSubDomains after an audit. Consider preload last, deliberately.

Mistake 4: leaking URLs through the Referer header

What it protects: when a user clicks a link from your site to another site, the browser can send the page they came from in the Referer header. If that URL contains something sensitive, like a password reset token, a signed download link or an invoice ID, you've just shared it with a third party, or with every external script that loads on the page.

Modern browsers now default to a safer behaviour, but defaults differ and change. Being explicit costs one line.

The fix: Referrer-Policy: strict-origin-when-cross-origin sends only your origin (not the full path) to other sites, and nothing at all when going from HTTPS to HTTP. For especially sensitive pages, no-referrer. And, separately, keep secrets out of URLs where you can.

Mistake 5: never saying which browser features you use

What it protects: Permissions-Policy controls powerful browser features: camera, microphone, geolocation, payment and others. If your site never uses the camera, say so: camera=() disables it for your page and any iframes inside it. If a compromised third-party script ever tries to ask for it, the browser refuses.

It's a small header that shrinks the damage any injected script can do. Few sites send it.

Mistake 6: forgetting the small ones (and keeping the dead ones)

  • X-Content-Type-Options: nosniff stops browsers from "guessing" a file's type. Especially important if users can upload files: you don't want an uploaded "image" executed as a script.
  • X-XSS-Protection is dead. The browser filter it controlled has been removed from modern browsers and could itself cause problems. Don't add it; CSP is the replacement.
  • X-Powered-By: PHP/8.x tells attackers your exact version for free. Set expose_php = Off in php.ini.

A Laravel middleware for the basics

Here's a simplified starting point. The CSP is in report-only mode on purpose, so it won't break anything on day one:

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

        $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'SAMEORIGIN');
        $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
        $response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
        $response->headers->set('Content-Security-Policy-Report-Only',
            "default-src 'self'; frame-ancestors 'self'; object-src 'none'; base-uri 'self'");

        return $response;
    }
}

Register it for web routes in bootstrap/app.php in Laravel 12:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [SecurityHeaders::class]);
})

Adjust before copying: if your site legitimately uses the camera or must be framed by a partner, the values above will need changing, and the HSTS line assumes every subdomain is on HTTPS.

Pick one place for headers. Set them in the app or in Nginx/Apache, not both by accident. Two different CSP headers aren't merged; the browser enforces both, so the effective policy is stricter than either one, and very confusing to debug. Also remember that static files served directly by the web server never pass through Laravel middleware.

How to check your own site

Open the browser's developer tools, go to the Network tab, click the main document request and read the response headers. Or run curl -I https://yoursite.com. Free online scanners will also grade your headers and explain each missing one.

The short version:

  • CSP in report-only first, then enforce, with nonces instead of 'unsafe-inline'.
  • frame-ancestors for clickjacking, plus X-Frame-Options for old browsers.
  • HSTS with a careful rollout of includeSubDomains and preload.
  • Explicit Referrer-Policy and Permissions-Policy.
  • nosniff on, X-XSS-Protection and X-Powered-By gone.

Which header was the hardest to roll out on your site? For most teams I know, it's CSP, and I'm curious what broke first for you.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close