Files
crewli/api/tests/Feature/Security/CspHeaderTest.php
bert.hausmans 940297f214 security: implement CSP headers (API middleware + Nginx configs + dev meta tags)
API middleware:
- SecurityHeaders now sets Content-Security-Policy from config/security.php
- Default API policy: "default-src 'none'; frame-ancestors 'none'"
- Supports report-only mode via CSP_REPORT_ONLY env var
- Policy value configurable via CSP_POLICY env var

Nginx deployment configs (deploy/nginx/):
- security-headers.conf: shared headers for all server blocks
- csp-api.conf: restrictive JSON-only policy for api.crewli.app
- csp-spa.conf: SPA policy for app/admin (self + unsafe-inline styles)
- csp-portal.conf: portal policy matching SPA

Development:
- CSP meta tags added to all three index.html files
- Includes 'unsafe-inline' + 'unsafe-eval' for Vite HMR/loader script
- Each app allows its own ws:// port for HMR websocket

Resolves security finding A13-9.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:14:37 +02:00

66 lines
1.9 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Security;
use Tests\TestCase;
final class CspHeaderTest extends TestCase
{
public function test_api_responses_include_csp_header(): void
{
$response = $this->getJson('/api/v1/');
$response->assertHeader('Content-Security-Policy');
}
public function test_api_csp_is_restrictive(): void
{
$response = $this->getJson('/api/v1/');
$csp = $response->headers->get('Content-Security-Policy');
$this->assertStringContainsString("default-src 'none'", $csp);
$this->assertStringContainsString("frame-ancestors 'none'", $csp);
}
public function test_csp_header_matches_config(): void
{
$expectedCsp = config('security.csp');
$response = $this->getJson('/api/v1/');
$response->assertHeader('Content-Security-Policy', $expectedCsp);
}
public function test_report_only_mode_uses_report_only_header(): void
{
config(['security.csp_report_only' => true]);
$response = $this->getJson('/api/v1/');
$response->assertHeader('Content-Security-Policy-Report-Only');
$this->assertNull($response->headers->get('Content-Security-Policy'));
}
public function test_no_csp_header_when_policy_is_null(): void
{
config(['security.csp' => null]);
$response = $this->getJson('/api/v1/');
$this->assertNull($response->headers->get('Content-Security-Policy'));
$this->assertNull($response->headers->get('Content-Security-Policy-Report-Only'));
}
public function test_no_csp_header_when_policy_is_empty(): void
{
config(['security.csp' => '']);
$response = $this->getJson('/api/v1/');
$this->assertNull($response->headers->get('Content-Security-Policy'));
$this->assertNull($response->headers->get('Content-Security-Policy-Report-Only'));
}
}