Security headers e CSP no Laravel

1. Fundamentos de Security Headers em Aplicações Web

Security headers são cabeçalhos HTTP que instruem o navegador sobre como se comportar ao carregar e processar o conteúdo de uma aplicação web. Em aplicações PHP, especialmente aquelas construídas com Laravel, esses headers são essenciais para mitigar ataques como XSS (Cross-Site Scripting), clickjacking e MIME-type sniffing.

Os principais security headers que toda aplicação Laravel deveria implementar são:

  • X-Content-Type-Options: Impede que navegadores interpretem arquivos como um tipo MIME diferente do declarado. Valor recomendado: nosniff
  • X-Frame-Options: Protege contra clickjacking ao controlar se a página pode ser exibida em iframes. Valor recomendado: DENY ou SAMEORIGIN
  • X-XSS-Protection: Ativa o filtro XSS embutido nos navegadores (embora muitos navegadores modernos estejam depreciando este header). Valor recomendado: 1; mode=block

Para inspecionar headers no navegador, utilize as DevTools (aba Network) ou ferramentas como securityheaders.com e Mozilla Observatory.

2. Implementando Security Headers no Laravel

A forma mais eficiente de aplicar security headers globalmente no Laravel é através de um middleware customizado. Vamos criar um middleware que adiciona os principais headers de segurança:

<?php

namespace App\Http\Middleware;

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

class SecureHeaders
{
    private array $headers = [
        'X-Content-Type-Options' => 'nosniff',
        'X-Frame-Options' => 'DENY',
        'X-XSS-Protection' => '1; mode=block',
        'Referrer-Policy' => 'strict-origin-when-cross-origin',
        'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()',
    ];

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

        foreach ($this->headers as $key => $value) {
            $response->headers->set($key, $value);
        }

        return $response;
    }
}

Registre o middleware no App\Http\Kernel:

// App\Http\Kernel.php
protected $middleware = [
    // ...
    \App\Http\Middleware\SecureHeaders::class,
];

Para aplicar apenas em rotas específicas, adicione ao array $routeMiddleware:

protected $routeMiddleware = [
    'secure.headers' => \App\Http\Middleware\SecureHeaders::class,
];

E use no arquivo de rotas:

Route::middleware('secure.headers')->group(function () {
    // Rotas protegidas
});

3. Content Security Policy (CSP): Conceitos e Diretivas Essenciais

CSP é uma camada adicional de segurança que ajuda a detectar e mitigar ataques XSS e injeção de dados. Funciona através de um header HTTP (Content-Security-Policy) que define fontes confiáveis para diferentes tipos de recursos.

Diretivas principais:

  • default-src: Serve como fallback para todas as diretivas não especificadas
  • script-src: Controla fontes permitidas para scripts JavaScript
  • style-src: Controla fontes permitidas para folhas de estilo
  • img-src: Controla fontes permitidas para imagens
  • connect-src: Controla fontes para conexões via fetch, XMLHttpRequest, WebSocket

Exemplo de política restritiva:

header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'");

Para testar violações sem bloquear recursos, use o modo report-only:

header("Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-violation");

4. Configurando CSP no Laravel com Pacotes Especializados

O pacote spatie/laravel-csp simplifica a implementação de CSP no Laravel. Instalação:

composer require spatie/laravel-csp

Publicar configuração:

php artisan vendor:publish --provider="Spatie\Csp\CspServiceProvider"

Crie uma política CSP personalizada:

<?php

namespace App\Csp;

use Spatie\Csp\Directive;
use Spatie\Csp\Keyword;
use Spatie\Csp\Policy;
use Spatie\Csp\Value;

class AppPolicy extends Policy
{
    public function configure()
    {
        $this
            ->addDirective(Directive::BASE, Keyword::SELF)
            ->addDirective(Directive::DEFAULT, Keyword::SELF)
            ->addDirective(Directive::SCRIPT, [
                Keyword::SELF,
                'https://cdnjs.cloudflare.com',
                'https://code.jquery.com',
            ])
            ->addDirective(Directive::STYLE, [
                Keyword::SELF,
                'https://fonts.googleapis.com',
                Keyword::UNSAFE_INLINE, // Evite em produção
            ])
            ->addDirective(Directive::IMG, [
                Keyword::SELF,
                'data:',
                'https://*.cloudfront.net',
            ])
            ->addDirective(Directive::CONNECT, Keyword::SELF)
            ->addDirective(Directive::FONT, [
                'https://fonts.gstatic.com',
            ])
            ->addDirective(Directive::OBJECT, Keyword::NONE)
            ->addDirective(Directive::FRAME, Keyword::SELF);
    }
}

Para usar nonces com scripts inline:

// No controller ou view
$nonce = csp_nonce();

// Na view Blade
<script nonce="{{ $nonce }}">
    console.log('Script seguro com nonce');
</script>

Configure no arquivo config/csp.php:

return [
    'policy' => App\Csp\AppPolicy::class,

    'report_only' => env('CSP_REPORT_ONLY', true),

    'report_uri' => env('CSP_REPORT_URI', '/csp-report'),
];

5. Lidando com Recursos Externos e Frameworks Front-end

Ao integrar CDNs, Google Fonts ou APIs externas, é necessário ajustar a política CSP. Exemplo para Google Fonts e Analytics:

$this
    ->addDirective(Directive::FONT, [
        'https://fonts.gstatic.com',
    ])
    ->addDirective(Directive::STYLE, [
        'https://fonts.googleapis.com',
    ])
    ->addDirective(Directive::SCRIPT, [
        'https://www.googletagmanager.com',
        'https://www.google-analytics.com',
    ])
    ->addDirective(Directive::IMG, [
        'https://www.google-analytics.com',
    ]);

Para Laravel Mix/Vite, gere hashes automáticos para assets compilados:

// No arquivo webpack.mix.js
mix.js('resources/js/app.js', 'public/js')
   .sass('resources/sass/app.scss', 'public/css')
   .options({
       cspHash: true
   });

Para Alpine.js ou Livewire com nonces:

// No layout Blade
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" 
        nonce="{{ csp_nonce() }}" defer></script>

// Ou com Livewire
@livewireScripts
<script nonce="{{ csp_nonce() }}">
    document.addEventListener('livewire:load', function () {
        // Código Livewire
    });
</script>

6. Monitoramento e Debug de Violações de CSP

Configure o report-uri para coletar relatórios de violação:

$this->addDirective(Directive::REPORT_URI, '/csp-report');

Crie uma rota dedicada no Laravel para processar os relatórios:

// routes/web.php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CspReportController;

Route::post('/csp-report', [CspReportController::class, 'store'])
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);

Controller para processar relatórios:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class CspReportController extends Controller
{
    public function store(Request $request)
    {
        $report = $request->input('csp-report');

        Log::channel('csp')->warning('CSP Violation', [
            'document_uri' => $report['document-uri'] ?? null,
            'blocked_uri' => $report['blocked-uri'] ?? null,
            'violated_directive' => $report['violated-directive'] ?? null,
            'effective_directive' => $report['effective-directive'] ?? null,
            'original_policy' => $report['original-policy'] ?? null,
            'source_file' => $report['source-file'] ?? null,
            'line_number' => $report['line-number'] ?? null,
            'column_number' => $report['column-number'] ?? null,
            'user_agent' => $request->userAgent(),
            'ip' => $request->ip(),
        ]);

        return response()->json(['status' => 'ok'], 204);
    }
}

Configure o canal de log no config/logging.php:

'channels' => [
    'csp' => [
        'driver' => 'daily',
        'path' => storage_path('logs/csp.log'),
        'level' => 'warning',
        'days' => 30,
    ],
],

Ferramentas de terceiros como Report URI e Sentry oferecem integração com Laravel para análise avançada de violações CSP.

7. Boas Práticas e Testes de Segurança

Combine security headers com outras camadas de segurança:

// Middleware SecureHeaders completo
private array $headers = [
    'X-Content-Type-Options' => 'nosniff',
    'X-Frame-Options' => 'DENY',
    'X-XSS-Protection' => '1; mode=block',
    'Referrer-Policy' => 'strict-origin-when-cross-origin',
    'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()',
    'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains',
    'Cross-Origin-Embedder-Policy' => 'require-corp',
    'Cross-Origin-Opener-Policy' => 'same-origin',
    'Cross-Origin-Resource-Policy' => 'same-origin',
];

Testes com PHPUnit para validar headers:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class SecurityHeadersTest extends TestCase
{
    public function test_security_headers_are_present()
    {
        $response = $this->get('/');

        $response->assertHeader('X-Content-Type-Options', 'nosniff');
        $response->assertHeader('X-Frame-Options', 'DENY');
        $response->assertHeader('X-XSS-Protection', '1; mode=block');
        $response->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
        $response->assertHeader('Strict-Transport-Security');
    }

    public function test_csp_header_is_present()
    {
        $response = $this->get('/');

        $response->assertHeader('Content-Security-Policy');
    }
}

Checklist final para validação:

  1. Teste com securityheaders.com — obtenha nota A+
  2. Verifique no Mozilla Observatory — alvo: nota A
  3. Execute testes automatizados no pipeline CI/CD
  4. Monitore logs de violação CSP regularmente
  5. Revise e ajuste políticas periodicamente

Referências