Backend routing
Each request to the backend is eventually executed by a controller. A list of routes is defined which maps a given request to a controller and an action.
Routes are defined inside extensions, in the files
- Configuration/Backend/Routes.php for general requests
- Configuration/Backend/AjaxRoutes.php for Ajax calls
Here is an extract of EXT:backend/Configuration/Backend/Routes.php (GitHub):
<?php
use TYPO3\CMS\Backend\Controller;
return [
// Login screen of the TYPO3 Backend
'login' => [
'path' => '/login',
'access' => 'public',
'target' => Controller\LoginController::class . '::formAction',
],
// Main backend rendering setup (previously called backend.php) for the TYPO3 Backend
'main' => [
'path' => '/main',
'referrer' => 'required,refresh-always',
'target' => Controller\BackendController::class . '::mainAction',
],
// ...
];
So, a route file essentially returns an array containing route mappings. A route
is defined by a key, a path, a referrer and a target. The "public" access
property indicates that no authentication is required for that action.
Note
The current route object is available as a route attribute in the PSR-7 request object of every
backend request. It is added through the PSR-15 middleware stack and can be
retrieved using $request->get
.
Backend routing and cross-site scripting
Public backend routes (those having option 'access' => 'public'
) do not
require any session token, but can be used to redirect to a route that requires
a session token internally. For this context, the backend user logged in must
have a valid session.
This scenario can lead to situations where an existing cross-site scripting vulnerability (XSS) bypasses the mentioned session token, which can be considered cross-site request forgery (CSRF). The difference in terminology is that this scenario occurs on same-site requests and not cross-site - however, potential security implications are still the same.
Backend routes can enforce the existence of an HTTP referrer header by adding a
referrer
to routes to mitigate the described scenario.
'main' => [
'path' => '/main',
'referrer' => 'required,refresh-empty',
'target' => Controller\BackendController::class . '::mainAction'
],
Values for referrer
are declared as a comma-separated list:
required
enforces existence of HTTPReferer
header that has to match the currently used backend URL (for example,https://
), the request will be denied otherwise.example. org/ typo3/ refresh-
triggers an HTML-based refresh in case HTTPempty Referer
header is not given or empty - this attempt uses an HTML refresh, since regular HTTPLocation
redirect still would not set a referrer. It implies this technique should only be used on plain HTML responses and will not have any impact, for example, on JSON or XML response types.
This technique should be used on all public routes (without session token) that internally redirect to a restricted route (having a session token). The goal is to protect and keep information about the current session token internal.
The request sequence in the TYPO3 Core looks like this:
- HTTP request to
https://
having a valid user sessionexample. org/ typo3/ - Internally public backend route
/login
is processed - Internally redirects to restricted backend route
/main
since an existing and valid backend user session was found + HTTP redirect tohttps://
+ exposing the token is mitigated withexample. org/ typo3/ main?token=... referrer
route option mentioned above
Attention
Please keep in mind these steps are part of a mitigation strategy, which requires to be aware of mentioned implications when implementing custom web applications.
Dynamic URL parts in backend URLs
New in version 12.1
Backend routes can be registered with path segments that contain dynamic parts, which are then resolved into a PSR-7 request attribute called routing.
These routes are defined within the route path as named placeholders:
<?php
use MyVendor\MyExtension\Controller\MyRouteController;
return [
'my_route' => [
'path' => '/my-route/{identifier}',
'target' => MyRouteController::class . '::handle',
],
];
Within a controller (we use here a non-Extbase controller as example):
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class MyRouteController
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$routing = $request->getAttribute('routing');
$myIdentifier = $routing['identifier'];
$route = $routing->getRoute();
// ...
}
}
Generating backend URLs
Using the UriBuilder API, you can generate any kind of URL for the backend, may
it be a module, a typical route or an Ajax call. Therefore use either
build
or build
. The
Uri
then returns a PSR-7 conform Uri
object that can be
cast to a string when needed. Furthermore, the Uri
automatically
generates and applies the mentioned session token.
Example within a controller (we use here a non-Extbase controller):
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;
final class MyRouteController
{
public function __construct(
private readonly UriBuilder $uriBuilder,
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
// .. do some stuff
// Using a route identifier
$uri = $this->uriBuilder->buildUriFromRoute(
'web_layout',
['id' => 42],
);
// Using a route path
$uri = $this->uriBuilder->buildUriFromRoutePath(
'/record/edit',
[
'edit' => [
'pages' => [
123 => 'edit',
],
],
],
);
// ... do some other stuff
}
}
Sudo mode
New in version 12.4
Starting with TYPO3 v12.4 a the sudo mode, like for the install tool, can be request for arbitrary backend modules.
You can configure the sudo mode in your backend routing like this:
<?php
use MyVendor\MyExtension\Handlers\MyHandler;
use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessLifetime;
return [
'my-route' => [
'path' => '/my/route',
'target' => MyHandler::class . '::process',
'sudoMode' => [
'group' => 'mySudoModeGroup',
'lifetime' => AccessLifetime::S,
],
],
];
More information
Please refer to the following resources and look at how the TYPO3 source code handles backend routing in your TYPO3 version.