Build secure Content Security Policy headers with nonce-based script execution and strict defaults.
use Zappzarapp\Security\Csp\HeaderBuilder;
use Zappzarapp\Security\Csp\Directive\CspDirectives;
use Zappzarapp\Security\Csp\Nonce\NonceGenerator;
$generator = new NonceGenerator();
$csp = HeaderBuilder::build(CspDirectives::strict(), $generator);
header("Content-Security-Policy: {$csp}");
// Use nonce in HTML
$nonce = $generator->get();
echo "<script nonce=\"{$nonce}\">console.log('Safe!');</script>";| Class | Description |
|---|---|
HeaderBuilder |
Builds CSP header strings |
CspDirectives |
Immutable CSP configuration value object |
NonceGenerator |
Instance-based cryptographic nonce generator |
NonceRegistry |
Static singleton for simple usage |
NonceProvider |
Interface for dependency injection |
SecurityPolicy |
Enum for security policy levels |
ResourceDirectives |
Resource fetch directive configuration |
NavigationDirectives |
Navigation directive configuration |
ReportingConfig |
CSP reporting configuration |
Four security levels via SecurityPolicy enum:
| Policy | unsafe-eval | unsafe-inline | Use Case |
|---|---|---|---|
STRICT |
No | No | Production (default) |
LENIENT |
Yes | Yes | Development, legacy apps |
UNSAFE_EVAL |
Yes | No | Frameworks requiring eval (Vue 2) |
UNSAFE_INLINE |
No | Yes | Rare - avoid if possible |
use Zappzarapp\Security\Csp\Directive\CspDirectives;
use Zappzarapp\Security\Csp\SecurityPolicy;
// Strict (default) - recommended for production
$csp = new CspDirectives();
$csp = new CspDirectives(securityPolicy: SecurityPolicy::STRICT);
// Lenient - for development or legacy
$csp = new CspDirectives(securityPolicy: SecurityPolicy::LENIENT);
// Unsafe eval only - for frameworks like Vue 2
$csp = new CspDirectives(securityPolicy: SecurityPolicy::UNSAFE_EVAL);Convenient presets for common scenarios:
// Production: Strict nonce-based CSP
$csp = CspDirectives::strict();
// Development: Lenient with hot reload support
$csp = CspDirectives::development('localhost:5173');
// Legacy: For frameworks requiring eval
$csp = CspDirectives::legacy();use Zappzarapp\Security\Csp\Nonce\NonceGenerator;
$generator = new NonceGenerator();
$nonce = $generator->get(); // Same nonce for this instance
// Safe for long-running processes (Swoole, RoadRunner)
// Each instance generates its own 256-bit cryptographic nonceuse Zappzarapp\Security\Csp\Nonce\NonceRegistry;
$nonce = NonceRegistry::get();
// Reset for new request (required in long-running processes)
NonceRegistry::reset();use Zappzarapp\Security\Csp\Nonce\NonceProvider;
use Zappzarapp\Security\Csp\Nonce\NullNonce;
// For testing - no nonce in output
$csp = HeaderBuilder::build(new CspDirectives(), new NullNonce());
// Custom provider
class MyNonceProvider implements NonceProvider {
public function get(): string {
return $this->frameworkNonce;
}
}use Zappzarapp\Security\Csp\Directive\CspDirectives;
use Zappzarapp\Security\Csp\Directive\ResourceDirectives;
// Fluent API
$csp = (new CspDirectives())
->withImgSrc("'self' data: https://cdn.example.com")
->withFontSrc("'self' https://fonts.gstatic.com")
->withConnectSrc("'self' https://api.example.com");
// Or via ResourceDirectives
$resources = new ResourceDirectives(
img: "'self' https://images.example.com",
font: "'self' https://fonts.gstatic.com",
connect: "'self' https://api.example.com",
media: "'self'",
worker: "'self' blob:",
frame: "'self' https://embed.example.com"
);
$csp = (new CspDirectives())->withResources($resources);use Zappzarapp\Security\Csp\Directive\NavigationDirectives;
$navigation = new NavigationDirectives(
frameAncestors: "'none'", // Who can embed this page
baseUri: "'self'", // Restrict <base> tag
formAction: "'self'" // Form submission targets
);
$csp = (new CspDirectives())->withNavigation($navigation);// Nonce is auto-injected if not present
$csp = (new CspDirectives())
->withScriptSrc("'self' https://trusted-cdn.com")
->withStyleSrc("'self' https://fonts.googleapis.com");// Production with real-time features
$csp = (new CspDirectives())->withWebSocket('api.example.com:443');
// Development with hot reload
$csp = CspDirectives::development('localhost:5173');Note: Use host:port format, not full URL.
Test policies without blocking:
// Report-only header (violations logged, not blocked)
$header = HeaderBuilder::buildReportOnlyHeader(new CspDirectives());
header($header);use Zappzarapp\Security\Csp\Directive\ReportingConfig;
$reporting = new ReportingConfig(
uri: '/csp-report', // Legacy report-uri
endpoint: 'csp-endpoint', // Modern report-to
upgradeInsecure: true // Upgrade HTTP to HTTPS
);
$csp = new CspDirectives(reporting: $reporting);
// Or fluent
$csp = (new CspDirectives())
->withReportUri('/csp-report')
->withReportTo('csp-endpoint');| Directive | Default Value |
|---|---|
default-src |
'self' |
script-src |
'self' 'nonce-...' 'strict-dynamic' |
style-src |
'self' 'nonce-...' |
img-src |
'self' data: |
object-src |
'none' |
frame-ancestors |
'self' |
base-uri |
'self' |
form-action |
'self' |
upgrade-insecure-requests |
Enabled |
use Zappzarapp\Security\Csp\HeaderBuilder;
use Zappzarapp\Security\Csp\Directive\CspDirectives;
use Zappzarapp\Security\Csp\Directive\ReportingConfig;
use Zappzarapp\Security\Csp\Nonce\NonceGenerator;
$generator = new NonceGenerator();
$directives = (new CspDirectives(
reporting: new ReportingConfig(uri: '/csp-violations')
))
->withImgSrc("'self' data: https://cdn.example.com")
->withFontSrc("'self' https://fonts.gstatic.com")
->withConnectSrc("'self' https://api.example.com")
->withWebSocket('api.example.com:443');
$csp = HeaderBuilder::build($directives, $generator);
header("Content-Security-Policy: {$csp}");
$nonce = $generator->get();
?>
<!DOCTYPE html>
<html>
<head>
<script nonce="<?= $nonce ?>">
console.log('Secure inline script!');
</script>
<style nonce="<?= $nonce ?>">
body { margin: 0; }
</style>
</head>
<body>...</body>
</html>// WRONG: Script blocked
echo "<script>alert('blocked');</script>";
// CORRECT: Script allowed via nonce
echo "<script nonce=\"{$nonce}\">alert('allowed');</script>";Don't cache HTML containing nonces:
// WRONG: Stale nonce
$html = $cache->get('page', fn() => renderPage());
// CORRECT: Cache data, render fresh
$data = $cache->get('data', fn() => fetchData());
$html = renderPage($data, $generator->get());With strict-dynamic, URL allowlists in script-src are ignored:
// URL allowlist ignored by modern browsers with strict-dynamic
->withScriptSrc("'self' https://cdn.example.com")
// Instead, use nonce on script tags
echo "<script nonce=\"{$nonce}\" src=\"https://cdn.example.com/lib.js\"></script>";External nonces are validated to prevent injection:
// These throw InvalidDirectiveValueException:
NonceRegistry::set(''); // Empty
NonceRegistry::set("valid; malicious"); // Semicolon (CSP injection)
NonceRegistry::set("valid\nX-Header:"); // Newline (header injection)
NonceRegistry::set("valid' 'unsafe"); // Quote (CSP injection)
// Valid:
NonceRegistry::set('abc123XYZ'); // Alphanumeric
NonceRegistry::set('dGVzdC1ub25jZQ=='); // Base64- Use STRICT policy - Default to strictest settings, relax only when necessary
- Always use nonces - Never rely on
'unsafe-inline'in production - Test in report-only - Deploy new policies in report-only mode first
- Monitor violations - Set up CSP reporting endpoints
- 256-bit nonces - Generated with
random_bytes(32)for cryptographic security - Reset in async - Call
NonceRegistry::reset()in Swoole/RoadRunner