How to get Forge hosted Laravel application Security Headers perfect

Security

To get your security Headers perfect, you'll need the right tools, starting with a scanner.

Use this scanner, before, and then after:

https://securityheaders.com/

Before:

security-headers-report.png 82.91 KB
Fixing `Strict-Transport-Security`, `Referrer-Policy`, and `Permissions-Policy` is easy, but not so for `Content-Security-Policy`. Content security is very much dependant what you are using on your website. Each website has it's own stack and it's going to take quite a bit of trail and error to get it perfect. My recommendation is use Inspect in the browser and then copy the problems on by one into ChatGPT.

Here is the `SecureHeaders.php` middleware class for a fairly vanilla Filament PHP site with some jQuery in the mix.

You'll also notice a special `preload` at the end of the first header `set` command. See https://hstspreload.org/ if you want to pursue this.

The key to making this header work was to format it nicely with line feeds so we could slowly but surely trawl our site and make the corrections required.

<?php

namespace App\Http\Middleware;

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

class SecureHeaders
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);
        $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
        $response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
        $response->headers->set('Permissions-Policy', 'autoplay=(self), camera=(), encrypted-media=(self), fullscreen=(), geolocation=(self), gyroscope=(self), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=(self), usb=()');
        $cspWithNewlines = "
            default-src 'self';
            script-src 'self' cdn.jsdelivr.net platform.twitter.com 'unsafe-inline' 'unsafe-eval';
            style-src 'self' 'unsafe-inline' fonts.bunny.net cdn.jsdelivr.net fonts.googleapis.com;
            img-src 'self' * data:;
            font-src 'self' data: fonts.bunny.net cdn.jsdelivr.net fonts.gstatic.com;
            connect-src 'self';
            media-src 'self';
            frame-src 'self' *.bunny.net *.jsdelivr.net platform.twitter.com github.com *.youtube.com *.vimeo.com;
            object-src 'none';
            base-uri 'self';
            report-uri
        ";
        $response->headers->set('Content-Security-Policy', str_replace("\n", '', $cspWithNewlines));
        return $response;
    }
}
lang-php

Once you have a working config, you can add the middlewares in `/bootstrap/app.php`:

 ->withMiddleware(function (Middleware $middleware) {
        //
        $middleware->append(SecureHeaders::class);
    })
lang-php

Afterwards you should have this.. which actually took me hours to get perfect.

security-headers-pass.png 83.21 KB
See these blogs that helped me out:

A special mention also to Spatie who has https://github.com/spatie/laravel-csp. I couldn't get it working so reverted to formatted output with newlines.