Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Secureheaders Laravel Package

mikefrancis/laravel-secureheaders

mikefrancis/laravel-secureheaders adds configurable security headers to Laravel responses via middleware, with sensible defaults for CSP, HSTS, X-Frame-Options, Referrer-Policy, and related protections. Easily customize headers to harden apps against clickjacking, MIME sniffing, and other common web...

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a thin Laravel wrapper around mikefrancis/php-secureheaders. It registers a service provider, a facade, a middleware, and a publishable config file. It does not do anything Laravel-specific beyond wiring the underlying library into the HTTP kernel and config system — which is good, because it means the surface area you need to learn is tiny.

Install and register

composer require mikefrancis/laravel-secureheaders

On Laravel 5.5+ the service provider is auto-discovered. Register the middleware in app/Http/Kernel.php — for a first pass, put it in the global $middleware stack so every response (including 404s and API responses) gets the headers:

protected $middleware = [
    // ...
    \MikeFrancis\LaravelSecureHeaders\SecureHeadersMiddleware::class,
];

Publish the config so you can actually see what is being sent:

php artisan vendor:publish --provider="MikeFrancis\LaravelSecureHeaders\SecureHeadersServiceProvider"

Where to look first

  1. config/secureheaders.php — this is the whole feature surface: HSTS, CSP, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy. Read the keys and defaults before changing anything.
  2. vendor/mikefrancis/laravel-secureheaders/src/ — the middleware is ~30 lines; reading it tells you exactly when headers are applied and how config maps to the underlying library.
  3. vendor/mikefrancis/php-secureheaders/ — the real logic. If you need a header the config doesn't expose, this is where you find the fluent methods to call from a custom middleware.

First use case

The fastest realistic win is "make a security scanner (Mozilla Observatory, securityheaders.com) stop complaining." After registering the middleware and clearing caches, verify with curl rather than the browser, so you see the raw response:

php artisan config:clear
curl -sI https://your-app.test | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy'

If headers appear, you're done with the basic setup. Everything after that is tuning CSP and deciding which environments should get HSTS.

Implementation Patterns

Config-driven global middleware (the default)

Treat config/secureheaders.php as the source of truth for static headers — HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy. These rarely need per-request values, so keeping them in config and letting the middleware apply them everywhere is the cleanest pattern.

Environment-scoped headers

HSTS should not be sent in local or CI. Use the config plus AppServiceProvider (or a dedicated SecurityServiceProvider) to mutate values at boot:

// app/Providers/AppServiceProvider.php
public function boot(): void
{
    if (! app()->isProduction()) {
        config([
            'secureheaders.hsts.enabled' => false,
            'secureheaders.csp.enabled'  => false,
        ]);
    }
}

Disabling CSP locally is usually the right call because Vite's HMR client, Laravel Debugbar, and Telescope all inject inline scripts that will violate a strict policy and flood your console.

Route-group pattern for CSP

If you only want CSP on HTML responses (not API JSON), don't put the middleware globally. Move it into the web group, or a dedicated group:

protected $middlewareGroups = [
    'web' => [
        // ...
        \MikeFrancis\LaravelSecureHeaders\SecureHeadersMiddleware::class,
    ],
];

This avoids sending a CSP to clients that will never render a document and avoids confusing API consumers.

Per-request CSP adjustments via the facade

For pages that need a looser policy (an embed, a payment iframe, a marketing landing page with analytics), resolve the facade or underlying SecureHeaders instance in a route middleware and tweak before the response is sent:

use MikeFrancis\LaravelSecureHeaders\Facades\SecureHeaders;

public function handle($request, Closure $next)
{
    SecureHeaders::setCsp([
        'default-src' => ["'self'"],
        'script-src'  => ["'self'", 'https://js.stripe.com'],
        'frame-src'   => ['https://js.stripe.com'],
    ]);

    return $next($request);
}

Order matters: this middleware must run before SecureHeadersMiddleware, since the latter reads the accumulated state when it builds the response.

CSP nonces for inline scripts

The underlying library supports nonce-based CSP. The typical pattern is:

  1. Generate a nonce per request in a middleware (e.g. CspNonce), store it on the request or in a container binding.
  2. Configure script-src with 'nonce-{nonce}' (or use the library's nonce helper if available in your version).
  3. In Blade, emit <script nonce="{{ $nonce }}">.

For Vite/Blade stacks, wrap this in a Blade component so you can't forget the nonce. For Inertia/SPA apps this is mostly unnecessary — 'self' plus your asset origin is usually enough.

HSTS and HTTPS enforcement belong together

HSTS only works over HTTPS, so pair it with forced HTTPS and correct proxy trust. In AppServiceProvider:

if (app()->isProduction()) {
    URL::forceScheme('https');
}

And ensure App\Http\Middleware\TrustProxies lists your load balancer / Cloudflare ranges. If TrustProxies is wrong, $request->isSecure() returns false behind TLS termination, and you'll either skip HSTS or send it on plain HTTP where it's ignored.

Testing

Assert headers in feature tests — this catches accidental middleware removal during refactors:

it('sends hardened headers', function () {
    $this->get('/')
        ->assertHeader('X-Frame-Options', 'SAMEORIGIN')
        ->assertHeader('X-Content-Type-Options', 'nosniff');
});

it('sends a CSP in production config', function () {
    config(['secureheaders.csp.enabled' => true]);

    $response = $this->get('/');
    expect($response->headers->get('Content-Security-Policy'))->toContain("default-src 'self'");
});

Integration with upstream proxies

If you terminate TLS at nginx or Cloudflare and they already inject HSTS, don't add it twice. Duplicate Strict-Transport-Security headers with different max-age values are a real failure mode — browsers honor the first one. Either strip upstream headers or disable HSTS in this package and let the proxy own it.

Gotchas and Tips

The package is effectively unmaintained. Last release was May 2021. The illuminate/support constraint likely caps you at Laravel 8-ish. On Laravel 10/11 you'll either need --ignore-platform-reqs (risky), a fork, or — more sensible — port the 30 lines of middleware into your app or use spatie/laravel-csp. Check composer why-not before assuming it works. If you do install it on a newer Laravel, the service provider and middleware are simple enough that it usually just works, but you're on your own for support.

Middleware ordering. Place SecureHeadersMiddleware after TrustProxies and after HandleCors in the global stack. If you place it before HandleCors, CORS headers may overwrite or duplicate. If you place it before TrustProxies, scheme detection breaks and HSTS may be skipped.

CSP is the part that breaks your app. Common offenders:

  • Inline <script> and onclick= handlers (need nonce or 'unsafe-inline', and the latter is nearly as bad as no CSP).
  • eval in old libraries (needs 'unsafe-eval' — don't).
  • Google Analytics, Tag Manager, Facebook Pixel, Hotjar (need explicit script-src/connect-src/img-src entries).
  • Livewire (needs 'unsafe-eval' for Alpine in many setups — a known pain point).
  • Stripe/Recaptcha iframes (frame-src).

Roll out in Content-Security-Policy-Report-Only first if the underlying library exposes it, watch the console, then switch to enforcing.

Dev vs prod is not optional. Local development, Debugbar, Telescope, Ignition error pages, and Vite HMR all violate a strict CSP. Disable CSP (and HSTS) outside production. Nothing wastes an afternoon like debugging "why is my script blocked" when the answer is "you're in local."

HSTS preload is a one-way door. preload=true with includeSubDomains=true means every subdomain of your domain must serve HTTPS forever, and removal from browser preload lists takes months. Don't enable preload until you're sure. Start with max-age=300 in staging, then ramp to 31536000.

X-XSS-Protection should be 0, not 1; mode=block. The old library defaults may set 1; mode=block, which is now considered harmful (it can introduce XSS in old browsers). Override it in config or omit the header entirely.

X-Frame-Options vs CSP frame-ancestors. Modern browsers prefer frame-ancestors in CSP. Keep both if you support old browsers, but make sure they don't conflict — a DENY in XFO combined with a permissive frame-ancestors is confusing.

Config caching bites. After editing config/secureheaders.php, run php artisan config:cache in any environment where config is cached (or config:clear locally). A stale cached config is the #1 reason "my header change didn't apply."

Duplicate headers behind a proxy. Always inspect raw headers with curl -sI, not the browser DevTools (which sometimes collapses duplicates). If Cloudflare, nginx, or your PaaS adds headers, either remove them upstream or disable them here. For HSTS specifically, the browser takes the first Strict-Transport-Security header, so a shorter max-age from the proxy wins.

Extension points are thin but sufficient. Because the package is tiny, the practical customization path is:

  • Subclass SecureHeadersMiddleware and override the header assembly.
  • Or drop the facade in a decorator around the underlying MikeFrancis\SecureHeaders\SecureHeaders and bind it in the container.
  • Or, most commonly, forget the package and write ~20 lines in app/Http/Middleware/SecurityHeaders.php calling response->headers->set(...). On a modern Laravel this is usually the better long-term call, and it removes the dependency risk entirely.

Testing gotcha: assertHeader is exact-match. If the library includes whitespace or a trailing semicolon you didn't expect (nosniff; vs nosniff), assertions fail mysteriously. dd($response->headers->all()) once to see the real values, then lock them down.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo