Modern web security demands stricter Content Security Policy (CSP) rules. But inline scripts break traditional CSP implementations. That's where CSP nonces come in.
This guide shows you how to build custom TYPO3 middleware that generates CSP headers with nonces for inline scripts. We'll create a complete working solution with proper configuration and testing.
Why CSP Nonces Matter
Traditional CSP blocks all inline scripts by default. Nonces let you allow specific inline scripts by giving them a unique, unpredictable token. Each page load gets a fresh nonce.
The browser only executes inline scripts that match the current nonce. This stops XSS attacks while keeping your inline code functional.
Project Structure
Here's what we'll build:
Classes/
├── Middleware/
│ └── CspNonceMiddleware.php
├── Service/
│ └── CspNonceService.php
└── ViewHelpers/
└── CspNonceViewHelper.php
Configuration/
├── RequestMiddlewares.php
└── Services.yaml
Tests/
└── Functional/
└── Middleware/
└── CspNonceMiddlewareTest.php
The CSP Nonce Service
First, let's create a service that generates and manages nonces:
<?php
declare(strict_types=1);
namespace Vendor\Extension\Service;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Site\Entity\Site;
class CspNonceService implements SingletonInterface
{
private string $currentNonce = '';
public function generateNonce(): string
{
$this->currentNonce = base64_encode(random_bytes(16));
return $this->currentNonce;
}
public function getCurrentNonce(): string
{
if (empty($this->currentNonce)) {
$this->generateNonce();
}
return $this->currentNonce;
}
public function buildCspHeader(Site $site = null): string
{
$nonce = $this->getCurrentNonce();
// Basic CSP with script nonce
$csp = "default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'unsafe-inline';";
// Add site-specific policies if needed
if ($site) {
$config = $site->getConfiguration();
if (isset($config['csp_additional_sources'])) {
$additional = $config['csp_additional_sources'];
$csp .= " connect-src 'self' {$additional};";
}
}
return $csp;
}
}
The Middleware Implementation
Now the main middleware that adds CSP headers to responses:
<?php
declare(strict_types=1);
namespace Vendor\Extension\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Site\Entity\Site;
use Vendor\Extension\Service\CspNonceService;
class CspNonceMiddleware implements MiddlewareInterface
{
private CspNonceService $cspService;
public function __construct(CspNonceService $cspService)
{
$this->cspService = $cspService;
}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
// Generate nonce before processing request
$this->cspService->generateNonce();
// Continue with request processing
$response = $handler->handle($request);
// Only add CSP header for HTML responses
$contentType = $response->getHeaderLine('Content-Type');
if (!str_contains($contentType, 'text/html')) {
return $response;
}
// Get current site for site-specific policies
$site = $request->getAttribute('site');
// Build and add CSP header
$cspHeader = $this->cspService->buildCspHeader($site);
return $response->withHeader('Content-Security-Policy', $cspHeader);
}
}
ViewHelper for Templates
Create a ViewHelper to output nonces in your Fluid templates:
<?php
declare(strict_types=1);
namespace Vendor\Extension\ViewHelpers;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use Vendor\Extension\Service\CspNonceService;
class CspNonceViewHelper extends AbstractViewHelper
{
public static function renderStatic(
array $arguments,
\Closure $renderChildrenClosure,
RenderingContextInterface $renderingContext
): string {
$cspService = GeneralUtility::makeInstance(CspNonceService::class);
return $cspService->getCurrentNonce();
}
}
Configuration Files
Register the middleware in Configuration/RequestMiddlewares.php:
<?php
return [
'frontend' => [
'vendor/extension/csp-nonce' => [
'target' => \Vendor\Extension\Middleware\CspNonceMiddleware::class,
'after' => [
'typo3/cms-frontend/site',
],
'before' => [
'typo3/cms-frontend/output-compression',
],
],
],
];
Configure dependency injection in Configuration/Services.yaml:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
Vendor\Extension\:
resource: '../Classes/*'
Vendor\Extension\Service\CspNonceService:
public: true
Vendor\Extension\Middleware\CspNonceMiddleware:
arguments:
$cspService: '@Vendor\Extension\Service\CspNonceService'
Using Nonces in Templates
In your Fluid templates, add the nonce to inline scripts:
In your Fluid templates, add the nonce to inline scripts:
{namespace csp=Vendor\Extension\ViewHelpers}
<script nonce="{csp:cspNonce()}">
// Your inline JavaScript here
console.log('This script will execute');
</script>
<!-- This script will be blocked -->
<script>
console.log('This script will be blocked by CSP');
</script>
Per-Site CSP Policies
Configure site-specific CSP sources in your site configuration:
# config/sites/main/config.yaml
rootPageId: 1
base: 'https://example.com/'
csp_additional_sources: 'https://cdn.example.com https://analytics.google.com'
The middleware automatically picks up these settings and adds them to the CSP header.
Testing with Browser DevTools
- Open your TYPO3 site in Chrome or Firefox
- Press F12 to open DevTools
- Go to the Network tab and reload the page
- Find your page request and check the Response Headers
- Look for Content-Security-Policy header
You should see something like:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-ABC123xyz789'; style-src 'self' 'unsafe-inline';
Check the Console tab for any CSP violations. Blocked scripts will show red error messages.
Functional Testing
Create a test to verify your middleware works:
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Functional\Middleware;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\Extension\Middleware\CspNonceMiddleware;
use Vendor\Extension\Service\CspNonceService;
class CspNonceMiddlewareTest extends FunctionalTestCase
{
protected array $testExtensionsToLoad = [
'typo3conf/ext/your_extension',
];
/**
* @test
*/
public function middlewareAddsCspHeaderToHtmlResponse(): void
{
$cspService = new CspNonceService();
$middleware = new CspNonceMiddleware($cspService);
$request = new ServerRequest('https://example.com/');
$handler = new class() implements \Psr\Http\Server\RequestHandlerInterface {
public function handle(\Psr\Http\Message\ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface
{
return (new Response())
->withHeader('Content-Type', 'text/html; charset=utf-8');
}
};
$response = $middleware->process($request, $handler);
self::assertTrue($response->hasHeader('Content-Security-Policy'));
$cspHeader = $response->getHeaderLine('Content-Security-Policy');
self::assertStringContainsString('script-src', $cspHeader);
self::assertStringContainsString('nonce-', $cspHeader);
}
/**
* @test
*/
public function middlewareSkipsNonHtmlResponses(): void
{
$cspService = new CspNonceService();
$middleware = new CspNonceMiddleware($cspService);
$request = new ServerRequest('https://example.com/api/data');
$handler = new class() implements \Psr\Http\Server\RequestHandlerInterface {
public function handle(\Psr\Http\Message\ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface
{
return (new Response())
->withHeader('Content-Type', 'application/json');
}
};
$response = $middleware->process($request, $handler);
self::assertFalse($response->hasHeader('Content-Security-Policy'));
}
}
Run the tests with:
vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml Tests/Functional/
What's Next
This implementation gives you a solid foundation for CSP with nonces. You can extend it by:
- Adding style nonces (similar to script nonces)
- Creating admin interface for CSP policy management
- Adding CSP violation reporting endpoints
- Supporting hash-based CSP for static scripts
The middleware approach keeps CSP logic separate from your business code. And since it runs early in the request cycle, it catches all HTML responses automatically.
Your TYPO3 site now blocks unauthorized inline scripts while keeping your legitimate code working. That's modern web security done right.
Post a Comment