The NelmioSecurityBundle provides PHPUnit constraints and a trait to help you test the security headers in your application.
The test utilities are included in the bundle. You can use the SecurityHeadersAssertionsTrait in your functional tests.
The trait allows you to add security header assertions to any test case that extends WebTestCase:
<?php
use Nelmio\SecurityBundle\Test\SecurityHeadersAssertionsTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
class HomepageTest extends WebTestCase
{
use SecurityHeadersAssertionsTrait;
public function testHomepageHasSecurityHeaders(): void
{
$client = static::createClient([], ['HTTPS' => 'on']);
$client->request(Request::METHOD_GET, '/');
static::assertIsIsolated();
static::assertFrameOptions('DENY');
static::assertContentTypeOptions();
static::assertReferrerPolicy(['no-referrer', 'strict-origin-when-cross-origin']);
static::assertStrictTransportSecurity();
static::assertCspHeader();
}
}assertIsIsolated()
Verifies that the response is properly configured for cross-origin isolation, which enables powerful browser features like SharedArrayBuffer and high-precision timers.
static::assertIsIsolated();This checks:
- Cross-Origin-Resource-Policy: same-origin
- Cross-Origin-Embedder-Policy: require-corp
- Cross-Origin-Opener-Policy: same-origin
Cross-Origin Headers
// All cross-origin headers at once (all parameters are required)
static::assertCrossOriginHeaders('same-origin', 'require-corp', 'same-origin');
// Or individually
static::assertCrossOriginResourcePolicy('same-origin');
static::assertCrossOriginEmbedderPolicy('require-corp');
static::assertCrossOriginOpenerPolicy('same-origin');Frame Options
static::assertFrameOptions('DENY');
static::assertFrameOptions('SAMEORIGIN');Content Type Options
static::assertContentTypeOptions();Referrer Policy
static::assertReferrerPolicy(['no-referrer', 'strict-origin-when-cross-origin']);Strict Transport Security (HSTS)
// With default values (maxAge=31536000, includeSubDomains=true, preload=true)
static::assertStrictTransportSecurity();
// With custom values
static::assertStrictTransportSecurity(31536000, true, true);Content Security Policy
// Check that CSP header exists
static::assertCspHeader();
// Check for specific directives
static::assertCspHeader([
'default-src',
'script-src',
'style-src',
]);
// Check Content-Security-Policy-Report-Only instead
static::assertCspHeader(['default-src', 'script-src'], true);
// Check that CSP contains specific values
static::assertCspHeader(null, false, ["'self'", 'https://cdn.example.com']);
// Check that CSP does NOT contain unsafe values
static::assertCspHeader(null, false, [], ["'unsafe-inline'", "'unsafe-eval'"]);
// Combine all checks: directives, required values, and forbidden values
static::assertCspHeader(
['default-src', 'script-src'], // required directives
false, // not report-only
["'self'"], // must contain 'self'
["'unsafe-inline'"] // must NOT contain 'unsafe-inline'
);/**
* @dataProvider routeProvider
*/
public function testSecurityHeadersForAllRoutes(string $route): void
{
$client = static::createClient([], ['HTTPS' => 'on']);
$client->request('GET', $route);
static::assertIsIsolated();
static::assertFrameOptions('DENY');
static::assertContentTypeOptions();
}
public static function routeProvider(): iterable
{
yield 'homepage' => ['/'];
yield 'about' => ['/about'];
yield 'contact' => ['/contact'];
}public function testApiEndpointHasRelaxedCORP(): void
{
$client = static::createClient([], ['HTTPS' => 'on']);
$client->request('GET', '/api/public/data');
// API might use 'cross-origin' instead of 'same-origin'
static::assertCrossOriginHeaders('cross-origin', 'require-corp', 'same-origin');
}public function testHomepageHasCorrectCSP(): void
{
$client = static::createClient([], ['HTTPS' => 'on']);
$client->request('GET', '/');
// Verify specific CSP directives are present
static::assertCspHeader([
'default-src',
'script-src',
'style-src',
'img-src',
'connect-src',
'font-src',
]);
}
public function testReportOnlyCSP(): void
{
$client = static::createClient([], ['HTTPS' => 'on']);
$client->request('GET', '/');
// Test Content-Security-Policy-Report-Only header
static::assertCspHeader(['default-src', 'script-src'], true);
}
public function testCspDoesNotAllowUnsafeInline(): void
{
$client = static::createClient([], ['HTTPS' => 'on']);
$client->request('GET', '/');
// Ensure CSP does not contain unsafe values
static::assertCspHeader(
['default-src', 'script-src'],
false,
[],
["'unsafe-inline'", "'unsafe-eval'"]
);
}
public function testCspAllowsSpecificCdn(): void
{
$client = static::createClient([], ['HTTPS' => 'on']);
$client->request('GET', '/');
// Ensure CSP allows a specific CDN
static::assertCspHeader(
['script-src'],
false,
['https://cdn.example.com']
);
}You can also use the constraints directly with PHPUnit's assertThat():
use Nelmio\SecurityBundle\Test\Constraint\ResponseHasCrossOriginResourcePolicy;
$response = $client->getResponse();
static::assertThat(
$response,
new ResponseHasCrossOriginResourcePolicy('same-origin')
);All constraints are in the Nelmio\SecurityBundle\Test\Constraint namespace:
ResponseHasCrossOriginResourcePolicyResponseHasCrossOriginEmbedderPolicyResponseHasCrossOriginOpenerPolicyResponseHasFrameOptionsResponseHasContentTypeOptionsResponseHasReferrerPolicyResponseHasStrictTransportSecurityResponseHasContentSecurityPolicy
- Test all public endpoints: Ensure security headers are present on all your routes
- Test different configurations: If you have different header configurations for APIs vs frontend, test both
- Use data providers: Test multiple routes efficiently with PHPUnit data providers
- Be specific: Use individual assertions when you need precise control over expected values