Firewall Extension 

The Firewall extension protects your TYPO3 website against unwanted traffic, bots, and attacks. It blocks or limits requests based on IP address, path, headers, or other patterns, and bans clients after repeated abuse.

The extension is built on the phirewall package and adds everything you need in TYPO3: the request middleware, a backend module for block patterns, bans and statistics, ready-made rule presets, and dashboard widgets.

Start with Quick start if you want a working firewall in five minutes. Read Introduction to understand what the extension does and how it relates to phirewall.

Backend module 

The extension adds a backend module under System > Firewall. It is available to administrators only.

The module has three views. Switch between them with the View dropdown in the module's doc-header:

  • Patterns manages the static block patterns.
  • Blocked keys lists the clients that rules have banned automatically.
  • Statistics shows how much traffic the firewall blocked over time.

Patterns 

This view manages the static block patterns. The extension always adds them to the firewall as the blocklist rule typo3-blocklist, so they take effect even when no configuration file exists (see Configuration). Patterns are stored in the file config/system/phirewall.patterns.json (classic installation: typo3conf/system/phirewall.patterns.json).

Every change takes effect on the next request. No deployment and no cache flush are needed.

The pattern list 

The Active Patterns list shows one row per pattern with its kind, value, target, expiry date, creation date, and last change. A pattern that has passed its expiry date is highlighted and no longer blocks requests, until you remove it or run a prune (see below).

Add and edit patterns 

The form next to the list creates a new pattern. Pick a kind, enter the value, and save. To change a pattern, open it from the list, edit the fields, and save. The form checks the value before it stores the pattern and shows a clear message when something is wrong, for example an invalid IP address or a broken regular expression.

Pattern kinds 

A pattern's kind decides what part of the request it compares against.

ip
Blocks one exact client IP address, for example 203.0.113.10.
cidr
Blocks a whole IP range in CIDR notation, for example 203.0.113.0/24.
path_exact
Blocks requests whose path is exactly this value, for example /old-login.
path_prefix
Blocks requests whose path starts with this value, for example /wp-admin.
path_regex
Blocks requests whose path matches this regular expression, for example #^/(wp-admin|xmlrpc\.php)#.
header_exact
Blocks requests where a header has exactly this value. Put the header name in the target field, for example target User-Agent and value BadBot/1.0.
header_regex
Blocks requests where a header matches this regular expression. Put the header name in the target field, for example target User-Agent and value #(sqlmap|nikto)#i.
request_regex
Blocks requests where the regular expression matches a combined string of the path, the query string, and the request headers, for example #(union\s+select|<script)#i.

The target field is only used by the two header kinds. For every other kind you can leave it empty.

Expiry and prune 

The expiry date is optional. When set, it must lie in the future. An expired pattern stops blocking at once, but its row stays in the list so you can see it. The Prune button deletes all expired patterns in one step.

Integrity check 

The view checks the pattern file on every visit. When the file is broken, for example because it holds invalid data or a pattern with an unknown kind, a warning banner appears. The firewall silently skips the affected entries during request handling, so the banner is your signal to open the patterns file and fix or remove them.

Blocked keys 

This view lists the keys that fail2ban and allow2ban rules have banned automatically. A key is usually a client IP address. The bans are read live from the store that your configuration uses (see Storage), so the view is empty when you use the InMemoryCache, which keeps no state between requests.

Bans are grouped by the rule that created them. Each group carries a badge that shows the rule type, fail2ban or allow2ban. Inside a group every ban shows the key, the remaining time, and the exact time the ban ends. The bans with the least time left are listed first. Use the search field to find a single key across all groups.

The Unban button removes a single ban after a confirmation dialog and lets the key through again right away. When the behavior that triggered the ban continues, the rule bans the key again on the next matching request.

Blocklist matches do not appear here. A blocklist rule answers each matching request with a 403 response on the spot and keeps no ban, so there is nothing to list. To see blocklist activity, use the event log and the Statistics view.

Statistics 

This view answers one question: how much unwanted traffic the firewall blocked. It shows the number of attackers blocked today, a chart over time, and the rules and paths that triggered most often. For the full description of the recorded data, the privacy model, and the extension settings, see Statistics.

Common attacks 

Problem-first recipes: what to do when a specific kind of unwanted traffic hits your site. Each section names the right tool and links to the chapter with the details.

Spam and flooding on a contact form 

Form spam is almost always automated, so hardening the form itself beats chasing the constantly rotating IP addresses. In forms built with the TYPO3 Form Framework, start with the built-in honeypot (and a CAPTCHA where available); other form extensions ship equivalent protections. Beyond that:

  • For forms built with the TYPO3 Form Framework, add the Firewall: flood protection finisher. It reports every submission to the firewall and bans a client that submits faster than a configured threshold, catching both bots and real visitors hammering the form. This is the extension's dedicated answer to form flooding, see Form flood protection.
  • A single IP address or network keeps abusing the form: add a pattern of kind ip or cidr in the backend module, with an expiresAt a few days out so it cleans itself up.
  • Cap how often the form can be submitted with a throttle in config/system/phirewall.php, scoped to the page that holds the form so it cannot affect the rest of the site:

    $config->throttles->add(
        name: 'contact-form-flood',
        limit: 3,
        period: 600,
        key: function (\Psr\Http\Message\ServerRequestInterface $request): ?string {
            // Only count POST submissions to the contact page.
            if ($request->getMethod() !== 'POST'
                || !str_starts_with($request->getUri()->getPath(), '/contact')) {
                return null; // the rule does not apply to other requests
            }
            // getIndpEnv() applies the reverseProxyIP settings, so the
            // throttle keys on the real client IP behind a proxy or CDN.
            $clientIp = \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REMOTE_ADDR');
            return is_string($clientIp) && $clientIp !== '' ? $clientIp : null;
        },
    );
    Copied!

Throttles need a persistent store, see Storage.

Brute-force login attempts 

The firewall runs before TYPO3, so on its own it cannot tell a failed login from a normal request. Two approaches:

  • Count login posts. Ban clients that post to the login page again and again, no code needed: see the allow2ban recipe in Examples. Simple, but it counts successful and failed logins alike, so the threshold must stay generous.
  • Report failed logins. Precise, in two steps. First define the rule in config/system/phirewall.php:

    $config->fail2ban->add(
        'login-failures',
        threshold: 5,
        period: 300,
        ban: 3600,
        // Failures are reported explicitly, the filter never matches on
        // its own.
        filter: static fn(): bool => false,
    );
    Copied!

    Then report each failed login, for example from a listener on TYPO3's failed-login event, through the firewall Context aspect:

    use TYPO3\CMS\Core\Context\Context;
    use TYPO3\CMS\Core\Utility\GeneralUtility;
    
    GeneralUtility::makeInstance(Context::class)
        ->getAspect('firewall')
        ->recordFailure('login-failures');
    Copied!

    The firewall aspect is registered automatically in frontend requests, see Middleware. Only reported failures count, so legitimate users are never locked out by successful logins.

SQL injection and other request payload attacks 

Attacks that hide in the request content (query string, body, headers) need a rule engine rather than a block list. Install the OWASP Core Rule Set preset package and pick a paranoia level, see Presets.

A crawler is slowing down the site 

The right response depends on which crawler it is; the User-Agent header in your access log tells you.

  • Search engines and AI search bots (Googlebot, Bingbot, OpenAI's OAI-SearchBot, Anthropic's Claude-SearchBot) should generally not be blocked: they are how people find and get referred to your site. To ease the load instead: Bing and Yandex honour a Crawl-delay in robots.txt; Googlebot ignores it, but treats repeated 429/503 responses as an overload signal and temporarily lowers its crawl rate, so a throttle that answers with 429 is a valid way to slow it down.
  • AI training crawlers (GPTBot, ClaudeBot, CCBot) honour robots.txt, which is the appropriate place to opt out:

    User-agent: GPTBot
    Disallow: /
    
    User-agent: ClaudeBot
    Disallow: /
    
    User-agent: CCBot
    Disallow: /
    Copied!

    Blocking these controls whether your content is used for AI training; it does not affect whether AI tools can find or cite your site.

  • SEO crawlers (AhrefsBot, SemrushBot, MJ12bot, DotBot) can be rate limited or blocked with the bot control preset, see Presets, or opted out via robots.txt.

Traffic from specific countries 

Geo-blocking is best handled at the edge (CDN/WAF). Inside TYPO3 there is no reliable country signal, hand-maintained per-country IP ranges go stale fast, and even done well it is blunt: it blocks real visitors and VPN users while attackers switch countries. Filtering on behaviour rather than origin (scanner paths, throttles, fail2ban) catches malicious traffic regardless of where it comes from. If geo-blocking is a hard compliance requirement, it belongs at the hosting or CDN layer.

Which rule blocked a visitor? 

Open the backend module: Blocked keys lists the active fail2ban/allow2ban bans grouped by rule, Patterns the static blocks. Remove the matching entry to unblock the visitor immediately. The event log shows which rule matched historically.

On development or staging systems, diagnostic response headers name the matching rule on every blocked response:

$config->enableResponseHeaders();
$config->enableOwaspDiagnosticsHeader(); // adds the OWASP rule ID
Copied!

A blocked response then carries X-Phirewall (the block type) and X-Phirewall-Matched (the rule name).

Configuration 

The configuration file 

The firewall is configured in one PHP file:

  • Composer-based installation: config/system/phirewall.php
  • Classic installation: typo3conf/system/phirewall.php

The file returns a closure. The closure receives the TYPO3 event dispatcher and returns a configured Flowd\Phirewall\Config object:

<?php
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Store\PdoCache;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

return function (EventDispatcherInterface $eventDispatcher): Config {
    // 1. The store keeps counters for rate limiting and bans. See the Storage page.
    $cache = new PdoCache(GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('phirewall_cache')->getNativeConnection());

    $config = new Config($cache, $eventDispatcher);

    // 2. Add your rules here.
    $config->blocklists->add(
        name: 'block-wp-admin',
        callback: fn($request) => str_starts_with(strtolower($request->getUri()->getPath()), '/wp-admin')
    );

    return $config;
};
Copied!

The closure receives the TYPO3 event dispatcher. Pass it to the Config object as shown above; the effective configuration always uses the TYPO3 event dispatcher, so event logging and the Statistics view work even when your file does not pass it on.

All rule types (safelists, blocklists, throttles, fail2ban, allow2ban, tracks) and their options are documented in the phirewall documentation. They work exactly the same way inside this file. See Examples for TYPO3 recipes.

What the extension adds automatically 

The extension builds its defaults first and merges your configuration file on top. Your file wins on every name clash, so it can override each default by using its name.

Client IP resolver
When your configuration does not call $config->setIpResolver(), the extension sets a resolver that uses GeneralUtility::getIndpEnv('REMOTE_ADDR'). This applies TYPO3's reverseProxyIP settings, so rules see the real visitor address behind a reverse proxy or CDN. When no address can be resolved, the resolver returns null and rules that key on the client IP skip the request. Details: Trusted proxies.
Backend managed block patterns
The block patterns from the backend module are added first as the blocklist rule typo3-blocklist, and they stay active even when your configuration file is missing. A rule with the same name in your file replaces them, so only define a rule named typo3-blocklist when you want to take over the backend managed patterns yourself.

Behavior without a configuration file 

When the file is missing, the extension falls back to a default configuration. When the file exists but is broken, the extension logs the problem and uses the same fallback: a warning when the file does not return a closure or the closure does not return a Config object, an error when loading the file fails, for example with a syntax error or an exception:

  • Store: InMemoryCache (nothing persists between requests)
  • Rules: only the backend managed block patterns

The website keeps working. Note that rate limiting and bans need a real store, so create the configuration file for any protection beyond static block patterns.

Examples 

This page collects ready-to-use recipes for common tasks. Each one is a complete config/system/phirewall.php file and returns the closure described in Configuration.

All examples use the ApcuCache store, the first choice on a single server. Pick the store that fits your setup on the Storage page. Rate limiting and bans need a store that keeps state between requests, so they do not work with the InMemoryCache.

Safelist office and monitoring IPs 

Let trusted clients through before any other rule runs, for example your office network, an uptime monitor, or a load test runner. A safelist match ends the check at once, so these clients are never rate limited or banned.

<?php
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Store\ApcuCache;
use Psr\EventDispatcher\EventDispatcherInterface;

return function (EventDispatcherInterface $eventDispatcher): Config {
    $cache = new ApcuCache();
    $config = new Config($cache, $eventDispatcher);

    $config->safelists->ip('office-and-monitoring', [
        '203.0.113.10',
        '198.51.100.0/24',
    ]);

    return $config;
};
Copied!

Ban brute-force logins with allow2ban 

Ban a client that posts to the frontend login again and again. An allow2ban rule with a filter counts the requests that match the filter and lets them through, then bans the client once it crosses the threshold within the period. A fail2ban rule is the wrong tool here: it treats every match as malicious and answers each login post with a 403, which locks out real users. Replace /login with the path of the page that holds your felogin form.

<?php
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Store\ApcuCache;
use Psr\EventDispatcher\EventDispatcherInterface;

return function (EventDispatcherInterface $eventDispatcher): Config {
    $cache = new ApcuCache();
    $config = new Config($cache, $eventDispatcher);

    $config->allow2ban->add(
        name: 'felogin-brute-force',
        threshold: 5,
        period: 300,
        banSeconds: 900,
        filter: fn($request) => $request->getMethod() === 'POST'
            && str_starts_with($request->getUri()->getPath(), '/login'),
    );

    return $config;
};
Copied!

This bans a client for 15 minutes after 5 login posts within 5 minutes.

Ban request floods with allow2ban 

An allow2ban rule counts every request from a client, not only the ones that match a filter, and bans the client once it crosses the threshold in the period. Use it as a blunt guard against request floods. Keep the threshold well above what a normal visitor reaches, so real people are never caught.

<?php
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Store\ApcuCache;
use Psr\EventDispatcher\EventDispatcherInterface;

return function (EventDispatcherInterface $eventDispatcher): Config {
    $cache = new ApcuCache();
    $config = new Config($cache, $eventDispatcher);

    $config->allow2ban->add(
        name: 'request-flood',
        threshold: 240,
        period: 60,
        banSeconds: 600,
    );

    return $config;
};
Copied!

This bans a client for 10 minutes once it sends more than 240 requests in 60 seconds.

Throttle a search or JSON endpoint 

Limit how often a client may call an expensive endpoint, for example a site search or a JSON API. A throttle allows a fixed number of requests per period and answers further requests with a 429 response and a Retry-After header. The scope keeps the counter to the endpoint you name, so browsing the rest of the site does not add to it.

<?php
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Config\ClosureRequestMatcher;
use Flowd\Phirewall\Config\Rule\ThrottleRule;
use Flowd\Phirewall\Store\ApcuCache;
use Psr\EventDispatcher\EventDispatcherInterface;

return function (EventDispatcherInterface $eventDispatcher): Config {
    $cache = new ApcuCache();
    $config = new Config($cache, $eventDispatcher);

    $config->throttles->addRule(new ThrottleRule(
        name: 'search-endpoint',
        limit: 20,
        period: 60,
        keyExtractor: null,
        scope: new ClosureRequestMatcher(
            fn($request) => str_starts_with($request->getUri()->getPath(), '/api/search'),
        ),
    ));
    $config->enableRateLimitHeaders();

    return $config;
};
Copied!

enableRateLimitHeaders() adds the X-RateLimit-* headers so clients can see how much of their budget is left.

Combine a preset with your own rules 

Apply a ready-made preset and add your own rules on top. with() returns a new configuration, so assign it back to $config. Rules you add afterwards run alongside the preset. See Presets for the available packages.

<?php
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Store\ApcuCache;
use Flowd\PhirewallPresetOwaspCrs\ParanoiaLevel;
use Flowd\PhirewallPresetOwaspCrs\Presets as OwaspPresets;
use Psr\EventDispatcher\EventDispatcherInterface;

return function (EventDispatcherInterface $eventDispatcher): Config {
    $cache = new ApcuCache();
    $config = new Config($cache, $eventDispatcher);

    $config = $config->with(OwaspPresets::blocklist(ParanoiaLevel::Level1));

    $config->blocklists->add(
        name: 'cms-scanner-paths',
        callback: fn($request): bool => (bool)preg_match('#^/(wp-admin|wp-login\.php|xmlrpc\.php)(/|$)#i', $request->getUri()->getPath()),
    );

    return $config;
};
Copied!

Send a custom response for blocked requests 

Replace the default 403 body with your own message and headers. The closure receives the rule name, the rule type, and the request, and returns a PSR-7 response. Keep the 403 status so crawlers and caches treat the request as blocked.

<?php
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Config\Response\ClosureBlocklistedResponseFactory;
use Flowd\Phirewall\Store\ApcuCache;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Http\ResponseFactory;
use TYPO3\CMS\Core\Http\StreamFactory;

return function (EventDispatcherInterface $eventDispatcher): Config {
    $cache = new ApcuCache();
    $config = new Config($cache, $eventDispatcher);

    $config->blocklists->add(
        name: 'block-wp-admin',
        callback: fn($request) => str_starts_with(strtolower($request->getUri()->getPath()), '/wp-admin'),
    );

    $config->blocklistedResponseFactory = new ClosureBlocklistedResponseFactory(
        fn(string $rule, string $type, $request) => (new ResponseFactory())
            ->createResponse(403)
            ->withHeader('Content-Type', 'text/plain; charset=utf-8')
            ->withBody((new StreamFactory())->createStream('Blocked by the firewall.')),
    );

    return $config;
};
Copied!

Lock the site down temporarily 

Close the site to everyone except a few addresses, for example during maintenance. The safelist is checked before the blocklist, so listed clients pass while the catch-all blocklist answers every other request with a 403. Remove both rules when you are done.

<?php
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Store\ApcuCache;
use Psr\EventDispatcher\EventDispatcherInterface;

return function (EventDispatcherInterface $eventDispatcher): Config {
    $cache = new ApcuCache();
    $config = new Config($cache, $eventDispatcher);

    $config->safelists->ip('maintenance-access', [
        '203.0.113.10',
    ]);
    $config->blocklists->add(
        name: 'lockdown',
        callback: fn($request) => true,
    );

    return $config;
};
Copied!

Forward firewall events to the TYPO3 log 

The extension records events in its own log (see Statistics). To also send them to the TYPO3 logging framework, register a PSR-14 listener for the phirewall events. This is a listener class in your own extension or site package, not part of the phirewall.php file.

The listener logs every blocked request:

<?php
declare(strict_types=1);

namespace MyVendor\MySitePackage\EventListener;

use Flowd\Phirewall\Events\BlocklistMatched;
use Psr\Log\LoggerInterface;

final class LogBlockedRequests
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function __invoke(BlocklistMatched $event): void
    {
        $this->logger->warning('Firewall blocked a request', [
            'rule' => $event->rule,
            'path' => $event->serverRequest->getUri()->getPath(),
        ]);
    }
}
Copied!

Register it in your extension's Configuration/Services.yaml:

MyVendor\MySitePackage\EventListener\LogBlockedRequests:
    tags:
        - name: event.listener
          identifier: 'my-firewall-log/blocklist-matched'
Copied!

TYPO3 reads the event to listen for from the type of the __invoke argument. On TYPO3 13 you can use the #[AsEventListener] attribute instead of the tag. The other events live in the Flowd\Phirewall\Events namespace, for example ThrottleExceeded and Fail2BanBanned.

FAQ 

Short answers to questions that come up when running the firewall in TYPO3.

For questions about the firewall engine itself (rule evaluation, stores, advanced features) see the phirewall FAQ.

Why is the TYPO3 backend not protected? 

The firewall runs as a frontend middleware. It covers the TYPO3 frontend only, not /typo3, the install tool, or files that the web server delivers directly (see Middleware). Protect the backend at the web server or network level, for example with an IP allow list for /typo3. TYPO3 also brings its own rate limiting for backend login attempts.

How does the firewall see the real client IP behind a proxy? 

The extension resolves the client IP through GeneralUtility::getIndpEnv('REMOTE_ADDR'), which applies the reverseProxyIP settings of your TYPO3 installation. Configure those settings once and both TYPO3 and the firewall see the real visitor address (see Trusted proxies).

Can I use the TYPO3 caching framework as a store? 

No. The store must implement PSR-16 and offer atomic counters for rate limiting and bans. The TYPO3 caching framework does neither, so counters would be lost or wrong. Use one of the stores in Storage, for example ApcuCache on a single server.

Should I commit phirewall.patterns.json? 

Usually not. The file holds the block patterns that editors manage in the backend module at runtime, so it is data, not code. When you deploy the file from Git, a deployment overwrites the changes made on the live site. Exclude it from deployment (for example through .gitignore) unless you never touch the patterns in the backend and manage them only in the file.

What happens if the configuration file has an error? 

The extension falls back to the default configuration: the InMemoryCache store and only the backend block patterns. The website keeps working. A file that does not return a closure, or whose closure does not return a Config object, logs a warning. A file that fails, for example with a syntax error or an exception, logs an error. Check the TYPO3 log after changing the file, and test changes on a staging system first.

Why is the blocked keys view empty? 

The view lists the bans that fail2ban and allow2ban rules created and that are still active. It is empty when you use the InMemoryCache, which keeps no state between requests, when your configuration has no fail2ban or allow2ban rules, or when no ban is active right now. When the InMemoryCache is active, the view shows a warning, and the firewall logs one when counter rules are registered on it. Blocklist matches never appear here, because they answer each request with a 403 and keep no ban.

Why is the statistics view empty? 

Most often event logging is switched off. When eventLogEnabled is off the view shows a hint and stays empty. Otherwise there may be no events yet in the selected time range, or the event types you look for are not in the eventLogTypes setting (see Statistics).

Does the extension work without Composer? 

Yes. Install the package from the TYPO3 Extension Repository. The TER package bundles the phirewall library, the three preset packages, and psr/simple-cache, so nothing else is needed (see Installation).

What does the firewall cost per request? 

Little. The middleware runs early, before TYPO3 resolves the site or the page, so a blocked request is answered at once and never reaches the CMS. For an allowed request the cost is evaluating your rules. Counter rules (throttle, fail2ban, allow2ban, track) each do one store lookup, so a fast store keeps the overhead low on busy sites. Event logging adds one database insert per recorded event, not per request, and the high-volume event types are off by default.

How do I disable the firewall in an emergency? 

Rename or empty the configuration file. The extension then falls back to the default configuration, and only the backend block patterns stay active. To drop those too, remove the patterns in the backend module. Inside the file you can also call $config->disable() to let every request pass without any check.

What if I locked myself out? 

The firewall never runs in the backend, so a rule can block your frontend access but never /typo3. Open the backend module and remove the pattern in the Patterns view, or lift the ban in the Blocked keys view. When a rule in the configuration file is the cause, edit or rename that file (see Backend module).

What about the event log and GDPR? 

The event log is built to hold as little personal data as possible. The client key, usually the IP address, is stored only as a SHA-256 hash. A readable address is kept only for real IP addresses and, by default, only in shortened form. Entries are deleted after the retention period. The log also stores the request path, method, host, and user agent. Review this against your privacy policy, keep IP anonymization on when you do not need full addresses, and set a retention that fits your needs (see Statistics).

Why are bans gone after the upgrade to 0.4? 

Version 0.4 updates the firewall engine from phirewall 0.3 to 0.7. Along the way the engine changed its internal cache key format. Active bans and running counters are forgotten once when you deploy the upgrade, then rebuild with the next matching requests. This reset happens only once (see Installation).

Form flood protection 

Ban clients that submit a form faster than a real visitor plausibly would. The extension ships a finisher for the TYPO3 Form Framework (EXT:form) that reports every submission of a form to the firewall. An allow2ban rule counts the submissions per client and bans the client once it crosses the threshold; the middleware then rejects further requests early, before TYPO3 boots.

The finisher counts every valid submission, not only suspicious ones: finishers run after the form validation, so rejected submissions are not counted. Unlike a honeypot or CAPTCHA, which target bots, it also stops real visitors who hammer a form. Use it alongside those measures, not instead of them.

Enable it 

  1. In Admin Tools > Settings > Extension Configuration > firewall, enable form flood protection. This registers an allow2ban rule named form-flood with the configured threshold (default 5 submissions), period (default 60 seconds), and ban duration (default 1 hour).
  2. In the form editor, add the Firewall: flood protection finisher to each form you want to protect.

Both steps are needed: without the rule, reported submissions are ignored; without the finisher, nothing reports. The client is identified by IP address, resolved with the same trusted-proxy handling as the rest of the firewall (see Trusted proxies).

Replace the default rule 

For full control over the rule, define form-flood in config/system/phirewall.php yourself. A rule from the configuration file always wins over the generated default:

$config->allow2ban->add(
    'form-flood',
    threshold: 3,
    period: 120,
    banSeconds: 7200,
    // Fed by the finisher; the rule never matches a request on its own.
    filter: static fn(): bool => false,
);
Copied!

Give a form its own counter 

By default every form using the finisher reports to the same form-flood rule, so their submissions share one counter per client: a visitor filling in several different forms counts towards a single limit, and the resulting ban applies site-wide. To count a form on its own, point its finisher at a separate allow2ban rule.

In the form editor, the Firewall: flood protection finisher has a Allow2ban rule identifier field. Enter a name other than form-flood (for example contact-form-flood) to give that form its own counter. The rule must exist, so define it in config/system/phirewall.php (the extension configuration only registers the default form-flood rule):

$config->allow2ban->add(
    'contact-form-flood',
    threshold: 3,
    period: 120,
    banSeconds: 7200,
    // Fed by the finisher; the rule never matches a request on its own.
    filter: static fn(): bool => false,
);
Copied!

A submission reported to a rule name that is not defined is ignored, so a typo in the field silently disables protection for that form; double-check the name matches the rule in phirewall.php.

Change the rule from code 

Before a submission is reported, the finisher dispatches the \Flowd\Typo3Firewall\Event\FloodProtectionFinisherTriggered event with the rule identifier the finisher resolved (the option above, or the default). A listener can change it, for example to derive the rule from the form, or set an empty identifier to skip the submission:

use Flowd\Typo3Firewall\Event\FloodProtectionFinisherTriggered;

final class UseStricterContactFormRule
{
    public function __invoke(FloodProtectionFinisherTriggered $event): void
    {
        $event->ruleIdentifier = 'contact-form-flood';
    }
}
Copied!

Register the class as an event listener in the Services.yaml of your site package. The rule it points to must be defined in config/system/phirewall.php.

Installation 

Requirements 

Extension version 0.4
TYPO3 12.4 LTS, 13.4 LTS, 14
PHP 8.3, 8.4, 8.5
Firewall engine flowd/phirewall 0.8

The firewall works with every database supported by TYPO3. Rate limiting and bans additionally need one of the stores described in Storage.

Installation with Composer 

composer require flowd/typo3-firewall
Copied!

Then activate the extension:

vendor/bin/typo3 extension:setup
Copied!

The frontend middleware is registered automatically. Continue with Quick start to create your first configuration file.

Optional packages 

Three preset packages add ready-made protection rules (see Presets):

composer require flowd/phirewall-preset-owasp-crs
composer require flowd/phirewall-preset-bots
composer require flowd/phirewall-preset-bad-ips
Copied!

The dashboard widgets need the TYPO3 dashboard:

composer require typo3/cms-dashboard
Copied!

Installation without Composer (TER) 

Install the extension from the TYPO3 Extension Repository using the extension manager, then activate it.

The TER package bundles everything the firewall needs: the phirewall library, the three preset packages, and psr/simple-cache. They live inside the extension under Resources/Private/Php/ComposerLibraries and are loaded automatically. No extra installation step is needed, and the presets are available without further setup.

Upgrade from 0.3 

Version 0.4 updates the firewall engine from phirewall 0.3 to 0.8. Review these points when upgrading:

New database table
Version 0.4 records firewall events in the new table tx_firewall_event. Update the database schema after the upgrade, for example with vendor/bin/typo3 extension:setup.
Counters and bans reset once
phirewall 0.5 changed its internal cache key format. Active bans and running rate limit counters are forgotten one time when you deploy the upgrade. They rebuild automatically with the next matching requests.
Bans trigger at the threshold
A fail2ban or allow2ban rule now bans when the threshold is reached, not one request later. With threshold: 5 the ban starts at the fifth matching request. Lower your thresholds by one if you relied on the old behavior.
Fail2Ban blocks every matching request
A fail2ban rule answers every request its filter matches with a 403, not only the request that reaches the threshold; the threshold controls when the client is banned outright. If a rule in your phirewall.php filters on something a legitimate request can carry (for example every POST to a login path), move it to an allow2ban rule with the same filter, which counts the matches but lets them pass until the threshold. Rules that match only clearly malicious traffic (scanner paths) block the probe on sight. Rules driven by RequestContext::recordFailure() (an empty filter) are unaffected.
New event type fail2ban_matched
A blocked-but-not-yet-banned fail2ban match is recorded as the new event type fail2ban_matched and counts towards the blocking statistics. It is enabled by default; adjust the logged types in the extension configuration if you do not want it.
$config->blocklists->owasp() was removed
The OWASP rule engine moved into the package flowd/phirewall-preset-owasp-crs. See Presets for the new way to enable it.
$config->safelists->trustedBots() was removed

Wire the matcher directly instead:

$config->safelists->addRule(new \Flowd\Phirewall\Config\Rule\SafelistRule(
    'trusted-bots',
    new \Flowd\Phirewall\Matchers\TrustedBotMatcher(cache: $cache)
));
Copied!
KeyExtractors::ip() is deprecated
Leave out the key argument of throttle, fail2ban, allow2ban, and track rules. They then count per client IP resolved through TYPO3 (see Trusted proxies).

Introduction 

What does the extension do? 

The Firewall extension adds a web application firewall to your TYPO3 website. It inspects every frontend request before TYPO3 processes it and can:

  • Block requests from specific IP addresses or IP ranges
  • Block requests for suspicious paths, headers, or patterns
  • Limit how often a client can call certain pages (rate limiting)
  • Ban clients temporarily after repeated abuse (like Fail2Ban)
  • Record firewall events and show statistics in the backend

Administrators manage static block patterns directly in the TYPO3 backend module. Changes take effect immediately, without a deployment.

Relation to phirewall 

The firewall engine is the open-source package flowd/phirewall. The extension wires it into TYPO3: it registers the request middleware, loads your configuration file, resolves the client IP through TYPO3, and adds the backend module and the event log.

This documentation covers everything TYPO3-specific. All engine features (rule types, stores, advanced options) are documented at https://phirewall.de/ and are used exactly the same way inside TYPO3.

Where things live 

  • Firewall configuration: config/system/phirewall.php (see Configuration)
  • Backend managed block patterns: config/system/phirewall.patterns.json (see Backend module)
  • Recorded firewall events: database table tx_firewall_event (see Statistics)

Middleware 

The extension registers the firewall as a PSR-15 middleware in the TYPO3 frontend request stack.

Position in the request stack 

The middleware runs early: after typo3/cms-frontend/timetracker and before typo3/cms-core/normalized-params-attribute. Two consequences follow from this position:

  • Blocked requests are cheap. The firewall answers before TYPO3 resolves the site, the page, or any content.
  • The firewall sees the raw request. TYPO3's normalized parameters and site handling have not run yet. That is why the extension resolves the client IP itself (see Trusted proxies).

What is protected, and what is not 

The middleware covers every request that reaches the TYPO3 frontend, including page requests, frontend login forms, and frontend APIs.

Not covered:

  • The TYPO3 backend (/typo3) and its login
  • The install tool (/typo3/install.php)
  • Files that the web server delivers directly, for example everything under fileadmin or _assets

Protect these entry points at the web server or network level, for example with IP allow lists for /typo3. TYPO3 itself brings rate limiting for backend login attempts.

Disable the firewall temporarily 

Return a configuration without rules, or rename the configuration file. The extension then falls back to the default configuration, and only the block patterns from the backend module stay active. To disable those too, remove the patterns in the backend module.

Read the firewall decision from PHP code 

A second middleware flowd/typo3-firewall-aspect exposes the firewall decision through the TYPO3 Context API as the firewall aspect. Like the firewall itself it runs in the frontend stack only, so the aspect is available in frontend requests and not in the backend. Application code can read the decision through the aspect:

use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$firewallAspect = GeneralUtility::makeInstance(Context::class)->getAspect('firewall');

// The FirewallResult of the current request:
$firewallResult = $firewallAspect->get('result');

// Report an application-level failure, for example a failed login,
// to a fail2ban rule defined in phirewall.php:
$firewallAspect->recordFailure('login-failures');

// Report a hit to an allow2ban rule, for example an expensive
// operation the firewall cannot see from the request alone:
$firewallAspect->recordHit('expensive-operation');
Copied!

recordFailure() and recordHit() count against the client IP by default; the firewall resolves it with your trusted-proxy settings after the handler has finished. Pass a key as second argument only when the rule should count something the firewall cannot derive from the request itself. Signals reported to a rule name that is not configured are ignored, so calling code does not need to check the configuration first.

The extension ships a phpstan configuration that maps getAspect('firewall') to the FirewallAspect class. Projects using phpstan/extension-installer pick it up automatically.

If you do not want the aspect registered at all, disable the middleware in the Configuration/RequestMiddlewares.php of your site package:

<?php

return [
    'frontend' => [
        'flowd/typo3-firewall-aspect' => [
            'disabled' => true,
        ],
    ],
];
Copied!

Presets 

Presets are ready-made rule bundles. You apply them to your configuration with $config = $config->with(...), one line per preset. Note the assignment: with() returns the combined configuration instead of changing $config in place.

Three preset packages are available. In a Composer-based installation, install the ones you want (see Installation); the TER package already bundles all three.

OWASP Core Rule Set 

The package flowd/phirewall-preset-owasp-crs detects common attack patterns like SQL injection, cross-site scripting, and path traversal, based on the OWASP Core Rule Set:

use Flowd\PhirewallPresetOwaspCrs\ParanoiaLevel;
use Flowd\PhirewallPresetOwaspCrs\Presets as OwaspPresets;

$config = $config->with(OwaspPresets::blocklist(ParanoiaLevel::Level1));
Copied!

Start with paranoia level 1. Higher levels detect more, but also produce more false positives. The fail2ban variant blocks each matching request just like the blocklist, but additionally bans a client key that keeps matching, so a repeat offender is locked out for the whole ban period:

$config = $config->with(OwaspPresets::fail2ban(ParanoiaLevel::Level1, threshold: 5, period: 600, ban: 3600));
Copied!

The rule set also covers requests for hundreds of sensitive files such as .env, .git, or .htpasswd, so you do not need rules of your own for those probes.

Bot control 

The package flowd/phirewall-preset-bots controls crawlers by their User-Agent:

use Flowd\PhirewallPresetBots\Presets as BotPresets;

$config = $config->with(
    BotPresets::blockAiCrawlers(),
    BotPresets::throttleSeoCrawlers(limit: 30, period: 60),
);
Copied!

This enforces policy for crawlers that identify truthfully. It is not a defense against hostile scrapers, which can fake any User-Agent.

Known bad IPs 

The package flowd/phirewall-preset-bad-ips blocks requests from a bundled snapshot of known attacker IP addresses:

use Flowd\PhirewallPresetBadIps\Presets as BadIpPresets;

$config = $config->with(BadIpPresets::blocklist());
Copied!

Blocking CMS scanner paths 

Bots constantly probe paths of other CMS products, for example /wp-admin or /xmlrpc.php. The OWASP rule set does not block these paths, because they are legitimate on a WordPress site. On a TYPO3 site they never are, so a single custom rule handles them:

$config->blocklists->add(
    name: 'cms-scanner-paths',
    callback: fn($request): bool => (bool)preg_match('#^/(wp-admin|wp-login\.php|wp-content|wp-includes|wordpress|xmlrpc\.php|phpmyadmin)(/|$)#i', $request->getUri()->getPath())
);
Copied!

Overriding preset rules 

Every preset rule has a namespaced name, for example preset.bots.*. A rule that you define later under the same name replaces the preset rule, so you can adjust single rules without giving up the rest of a preset.

Quick start 

Follow these steps to get a working firewall in a Composer-based TYPO3 project.

1. Install the extension 

composer require flowd/typo3-firewall
Copied!

The frontend middleware is registered automatically. Without a configuration file the firewall only enforces the block patterns managed in the backend module.

2. Create the configuration file 

Create the file config/system/phirewall.php. This minimal example blocks requests for paths that only scanners ask for:

<?php
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Store\PdoCache;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

return function (EventDispatcherInterface $eventDispatcher): Config {
    $cache = new PdoCache(GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('phirewall_cache')->getNativeConnection());
    $config = new Config($cache, $eventDispatcher);

    $config->blocklists->add(
        name: 'block-wp-admin',
        callback: fn($request) => str_starts_with(strtolower($request->getUri()->getPath()), '/wp-admin')
    );

    return $config;
};
Copied!

The PdoCache store creates its phirewall_cache table on its own the first time it runs. You must also declare the table in the ext_tables.sql of your site package, otherwise TYPO3 treats it as unused; Storage shows the definition and other store options.

The extension resolves the client IP for you through TYPO3, so the firewall sees the real visitor address behind a reverse proxy or CDN (see Trusted proxies).

3. Verify it works 

Request a blocked path. The firewall answers with status 403:

curl -i https://www.example.org/wp-admin/setup.php
Copied!

Regular pages keep working as before.

Next steps 

Statistics 

The extension records what the firewall does and shows it in the Statistics view of the backend module. This lets you see how much unwanted traffic your website receives and which rules and paths are hit most.

The event log 

Every firewall action is written as one row to the database table tx_firewall_event. A row holds the event type, the rule name, the ban type for ban events, the client key (see the privacy section below), the request host, path, and method, the user agent, rule numbers such as the threshold and the counter, for errors the exception class and message, and the time. It never stores a page, a session, or content. The recorded event types are:

blocklist_matched
A blocklist rule answered a request with a 403 response.
throttle_exceeded
A client sent more requests than a rate limit allows.
fail2ban_matched
A fail2ban rule blocked a request that matched its filter, before the ban threshold was reached.
fail2ban_banned
A fail2ban rule banned a client after repeated abuse.
allow2ban_banned
An allow2ban rule banned a client for too many requests.
safelist_matched
A safelist rule let a request through without further checks.
track_hit
A track rule counted a request without blocking it.
firewall_error
The firewall hit an internal error, for example when the store was unreachable.

The blocklist_matched, throttle_exceeded, fail2ban_matched, fail2ban_banned, and allow2ban_banned types count as a blocked attacker: each one rejected a request. They feed the numbers, the chart, and the top lists in the statistics view. The firewall_error type is recorded but not counted, because an internal error makes the firewall fail open by default, so the request was not blocked. The safelist_matched and track_hit types are high volume and switched off by default, because they fire on normal traffic and would fill the table quickly.

Settings 

The extension configuration controls what is logged and for how long. Open it under Admin Tools > Settings > Extension Configuration and select firewall.

eventLogEnabled (default: on)
Turns the event log on or off. When off, nothing is recorded and the statistics view stays empty.
eventLogTypes (default: blocklist_matched, throttle_exceeded, fail2ban_matched, fail2ban_banned, allow2ban_banned, firewall_error)
A comma-separated list of the event types to record. Add safelist_matched or track_hit only when you need them, and expect many more rows.
eventLogRetentionDays (default: 30)
How many days to keep entries. The prune command uses this value.
eventLogAnonymizeIp (default: on)
Stores client IP addresses in shortened form. The last part of the address is dropped, so a single visitor can no longer be identified.

Privacy and retention 

The event log is built to hold as little personal data as possible.

  • The key of a client, usually its IP address, is stored only as a SHA-256 hash. The hash lets the view count distinct clients without storing the raw key.
  • A readable address is kept in a separate field for the backend view, but only for real IP addresses and, with eventLogAnonymizeIp on, only in shortened form. Keys that are not IP addresses are never shown in readable form.
  • Entries are deleted after the retention period. The console command removes the old rows:

    vendor/bin/typo3 firewall:eventlog:prune
    Copied!

    The command deletes entries older than the configured retention. Pass --days to override it once, for example --days 7. Register the command as a scheduler task or a cron job so old entries are removed regularly.

How to read the statistics view 

When event logging is off, the view shows a hint and stays empty. Otherwise it shows:

  • Two large numbers at the top: Attackers blocked today, the count of distinct clients blocked since midnight, and Blocked requests, the total for the selected time range.
  • A time range switch with three options: the last 24 hours, 7 days, or 30 days. The 24-hour range groups the data by hour, the other two by day.
  • A Blocked requests over time chart. Each bar is stacked and split by event type, so you see at a glance whether blocklists, rate limits, or bans caused the traffic. Every type keeps the same color across ranges, and a legend below the chart names each color with its count.
  • A Recent blocked requests table with the last 20 blocking events in the selected range. Each row shows the time, the event type in the same color as the chart, the rule that fired, the request method and path, and the client. This answers which rule blocked a specific request.
  • A Top rules list with the five rules that blocked the most requests, and a Top blocked paths list with the five paths that attackers targeted most.
  • An Events by type list with the count per event type in the range.

Dashboard widgets 

The extension ships two widgets for the TYPO3 dashboard. They need the typo3/cms-dashboard package (see Installation) and read from the same event log.

Firewall: Blocked today
A single number: the distinct attackers blocked since midnight.
Firewall: Blocked requests
A bar chart of the blocked requests per day over the last seven days.

Statistics, blocked keys, and patterns 

The backend module shows firewall data in three ways. It helps to keep them apart:

  • Statistics is the history. It tells you what happened over time, even for events that are long over.
  • Blocked keys is the present. It lists the bans that are active right now and lets you lift them (see Backend module).
  • Patterns are the static rules you manage by hand. They do not depend on past traffic (see Backend module).

Storage 

Rate limiting and bans need a store that persists counters between requests. The store is the first argument of the Config object in your configuration file (see Configuration).

Which store should I use? 

ApcuCache First choice on a single server. Needs the APCu extension.
RedisCache First choice for multi-server setups. Needs a Redis server.
PdoCache Works without extra services, but opens a database connection for every request. Fallback only.
InMemoryCache Testing only. Nothing persists between requests.

Prefer ApcuCache or RedisCache. The firewall runs before TYPO3 boots, and with these stores a blocked request is answered without touching the database at all. PdoCache opens a database connection for every request, including the ones the firewall blocks, so an attack still creates load on the database, which is exactly what the firewall should prevent.

The TYPO3 caching framework cannot be used as a store: it does not implement PSR-16 and does not offer the atomic counters the firewall needs.

ApcuCache: single-server setups 

use Flowd\Phirewall\Store\ApcuCache;

$cache = new ApcuCache();
Copied!

APCu keeps counters in the memory of the PHP process manager. This is the fastest option, but the counters are per server: do not use it behind a load balancer, and note that a PHP restart clears all counters and bans.

RedisCache: multi-server setups 

use Flowd\Phirewall\Store\RedisCache;
use Predis\Client;

$cache = new RedisCache(new Client('redis://localhost:6379'));
Copied!

All servers share the same counters and bans. Requires the predis/predis package:

composer require predis/predis
Copied!

PdoCache: the TYPO3 database 

PdoCache stores its counters in a table of the TYPO3 database, using the connection settings of your installation, and creates that table on its own. It needs no additional service, which makes it the fallback when neither APCu nor Redis is available. Keep the performance cost from above in mind: every request opens a database connection before the firewall can block it.

use Flowd\Phirewall\Store\PdoCache;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$cache = new PdoCache(GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('phirewall_cache')->getNativeConnection());
Copied!

TYPO3 does not know this table, so the database analyzer in the install tool treats it as unused and offers to remove it, and with it all counters and bans. You must declare the table in the ext_tables.sql of your site package so TYPO3 knows it:

CREATE TABLE phirewall_cache (
    cache_key varchar(255) NOT NULL,
    cache_value text NOT NULL,
    expires_at bigint(20) DEFAULT NULL,
    PRIMARY KEY (cache_key)
);
Copied!

InMemoryCache: testing only 

use Flowd\Phirewall\Store\InMemoryCache;

$cache = new InMemoryCache();
Copied!

Counters live only for the current request. Block rules work, but rate limiting and bans do not, and the blocked keys view in the backend module always stays empty. Use it for local experiments or setups that only need static block rules.

Trusted proxies 

Why this matters 

Rules that work with the client IP (IP blocklists, rate limiting, bans) need the real visitor address. Behind a reverse proxy, a load balancer, or a CDN, the connecting address is the proxy, not the visitor. Without correct resolution, two things go wrong:

  • Every visitor appears as the same proxy IP. One ban then blocks everyone.
  • An attacker is not banned, because the counted address is the proxy.

The default: TYPO3 resolves the client IP 

The extension resolves the client IP through GeneralUtility::getIndpEnv('REMOTE_ADDR'). This applies the reverse proxy settings of your TYPO3 installation. Configure them once in config/system/settings.php (classic installation: typo3conf/system/settings.php) and both TYPO3 and the firewall see the real visitor address:

// config/system/settings.php
'SYS' => [
    'reverseProxyIP' => '203.0.113.10',   // the address of your proxy
    'reverseProxyHeaderMultiValue' => 'last',
],
Copied!

Details on these settings: TYPO3 reverse proxy configuration.

Without a proxy in front of your website no configuration is needed: the connecting address is already the visitor.

When no address can be resolved, the resolver returns null and rules that key on the client IP skip the request.

Using a different resolver 

Call $config->setIpResolver() in your configuration file to override the default, for example to use the phirewall TrustedProxyResolver with its own trust list:

use Flowd\Phirewall\Http\TrustedProxyResolver;
use Flowd\Phirewall\KeyExtractors;

$config->setIpResolver(KeyExtractors::clientIp(new TrustedProxyResolver(['10.0.0.0/8'])));
Copied!

This is only needed when the TYPO3 settings cannot express your setup. The resolver options are documented in the phirewall documentation.

Verify the resolution 

Add a temporary blocklist rule for your own IP address and request the website through the proxy:

$config->blocklists->ip('verify-my-ip', '198.51.100.7');
Copied!

When the request is blocked (status 403), the firewall sees your real address. When it passes, the proxy configuration is not applied: check the reverseProxyIP value. Remove the rule after the test.