How to Build Custom TYPO3 Middleware for Better CSP Security?

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

×

Got answer to the question you were looking for?