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 problems 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

The key to sanity was the last response to do the content. By having it nicely formatted 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');
        $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:

 ->withMiddleware(function (Middleware $middleware) {
        $middleware->alias([
            'role' => RoleMiddleware::class,
            'permission' => PermissionMiddleware::class,
            'role_or_permission' => RoleOrPermissionMiddleware::class,
        ]);
        $middleware->append(SecureHeaders::class);
    })
lang-php

After (many hours of fiddling):

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 output with newlines.