---
title: "Security considerations"
manual: "TYPO3 Explained"
version: "main"
permalink: "https://docs.typo3.org/permalink/t3coreapi:backend-modules-security@main"
source: "ExtensionArchitecture/HowTo/BackendModule/SecurityConsiderations.rst"
rendered: "2026-09-19T06:56:03+00:00"
---

# Security considerations {#backend-modules-security}

## Cross-Site-Request-Forgery (CSRF) {#backend-modules-security-cross-site-request}

### Overview {#backend-modules-security-cross-site-request-overview}

HTTP requests are typically categorized into at least two types:

-   **GET requests**: Used for retrieving data without altering the server's
    state, such as fetching webpages or performing searches.
-   **POST requests**: Used for actions that modify the server's state, such
    as creating accounts, submitting forms, or updating settings.

Cross-site attacks, such as Cross-Site Request Forgery (CSRF), exploit the
trust a web application has in a user's browser by tricking it into making
unintended requests. Setting cookies (primarily these used to provide
authentication) with the `SameSite=strict` attribute can mitigate these
attacks by ensuring that cookies are only sent with same-site requests.
However, certain edge cases, such as clicking a malicious link in a
standalone mail application, can still pose a risk, as cookies might be
sent in such scenarios.

This section explains how to enforce using `POST` for state-modifying actions
to mitigate CSRF risks and provides an example backend module implementation
to demonstrate best practices.

The example below demonstrates a module that renders a list of items and
provides a delete action. The previous implementation used `GET` links with
query parameters for the delete action, which modifies the server's state.
This should be replaced with `POST` requests for improved security.

### Asserting HTTP methods in custom module controllers {#backend-modules-security-cross-site-request-asserting-http}

#### Enforcing HTTP methods {#backend-modules-security-cross-site-request-asserting-http-enforcing-http}

The revised example below uses dedicated target handlers for each controller
action instead of a generic `handleRequest` handler.

****Revised** EXT:demo/Configuration/Backend/Modules.php**

```diff
  <?php
  use Example\Demo\Controller\CustomModuleController;

  return [
      'demo' => [
          'access' => 'user',
          'path' => '/module/dashboard',
          'iconIdentifier' => ...,  # Icon configuration here
          'labels' => ...,          # Label configuration here
          'routes' => [
-             '_default' => [
-                 'target' => CustomModuleController::class . '::handleRequest',
-             ],
+             '_default' => [
+                 'target' => CustomModuleController::class . '::listAction',
+             ],
+             'delete' => [
+                 'target' => CustomModuleController::class . '::deleteAction',
+             ],
          ],
      ],
  ];
```

To enforce appropriate HTTP methods, the revised examples make use of the
`\TYPO3\CMS\Core\Http\AllowedMethodsTrait`. `GET` is enforced for
`listAction`, and `POST` is required for `deleteAction`.

Besides that, the vague and unspecific `handleRequest` intermediate
dispatch method has been dropped in favour of having dedicated routes to
each controller action.

****Revised** EXT:demo/Classes/Controller/CustomModuleController.php**

```diff
  <?php
  namespace Example\Demo\Controller;

  use Example\Demo\Domain\Repository\ThingRepository;
  use TYPO3\CMS\Backend\Routing\UriBuilder;
  use TYPO3\CMS\Backend\Template\ModuleTemplate;
+ use TYPO3\CMS\Core\Http\AllowedMethodsTrait;
  use TYPO3\CMS\Core\Http\HtmlResponse;
  use TYPO3\CMS\Core\Http\RedirectResponse;

  class CustomModuleController
  {
+     use AllowedMethodsTrait;
+
      public function __construct(
          protected readonly UriBuilder $uriBuilder,
          protected readonly ThingRepository $thingRepository,
          protected readonly ModuleTemplate $moduleTemplate,
      ) {}

-     public function handleRequest(ServerRequestInterface $request): ResponseInterface
-     {
-         $action = $request->getQueryParams()['action']
-             ?? $request->getParsedBody()['action']
-             ?? 'list';
-         return $this->{$action . 'Action'}($request);
-     }
-
      public function listAction(ServerRequestInterface $request): ResponseInterface
      {
+         $this->assertAllowedHttpMethod($request, 'GET');
          $this->moduleTemplate->assignMultiple([
              'things' => $this->thingRepository->findAll(),
          ]);
          return $this->moduleTemplate->renderResponse('CustomModule/List');
      }

      public function deleteAction(ServerRequestInterface $request): ResponseInterface
      {
-         $thingId = $request->getQueryParams()['thing']
-             ?? $request->getParsedBody()['thing']
-             ?? null;
+         $thingId = $request->getParsedBody()['thing'] ?? null;
+         $this->assertAllowedHttpMethod($request, 'POST');

          // validate ID early
          if (!is_string($thingId) || $thingId === '') {
              return new HtmlResponse('Bad request', 400);
          }

          $this->thingRepository->removeById((int)$thingId);

          $listRoute = $this->uriBuilder
              ->buildUriFromRoute('demo', [], UriBuilder::ABSOLUTE_URL);
          return new RedirectResponse($listRoute);
      }
  }
```

#### Template example {#backend-modules-security-cross-site-request-asserting-http-template-example}

In the revised template, `POST`-based form buttons are used
instead of `GET` links for delete actions:

****Revised** EXT:demo/Resources/Private/Templates/ExtbaseModule/List.fluid.html**

```diff
  <ul>
  <f:for each="{things}" as="thing">
      <li>
          {thing.name}:
-         <a href="{f:be.uri(
-                 route: 'demo.delete',
-                 parameters: '{action: ‘delete’, thing: thing.uid}'
-             )" class="btn btn-default">delete</a>
+         <button
+             name="thing" value="{thing.uid}"
+             type="submit" form="demo-module-form-delete-action"
+             class="btn btn-default">delete</button>
      </li>
  </f:for>
  </ul>
+ <form
+     action="{f:be.uri(route: 'demo.delete')}" method="post"
+     id="demo-module-form-delete-action" class="hidden"></form>
```

> [!TIP]
> **Hint**
>
> The `<button form="identifier">` references the `<form id="identifier">`
> element. This allows multiple `<button>` elements to be used with a single
> `<form>`.

### Asserting HTTP methods in Extbase controllers {#backend-modules-security-cross-site-request-asserting-http-2}

#### Enforcing HTTP methods {#backend-modules-security-cross-site-request-asserting-http-2-enforcing}

The following example demonstrates enforcing HTTP methods in Extbase module
controllers using `AllowedMethodsTrait`:

****Revised** EXT:demo/Classes/Controller/ExtbaseModuleController.php**

```diff
  <?php
  namespace Example\Demo\Controller;

  use Example\Demo\Domain\Model\Thing;
  use Example\Demo\Domain\Repository\ThingRepository;
+ use TYPO3\CMS\Core\Http\AllowedMethodsTrait;
  use TYPO3\CMS\Backend\Template\ModuleTemplate;
  use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

  class ExtbaseModuleController extends ActionController
  {
+     use AllowedMethodsTrait;
+
      protected readonly ModuleTemplate $moduleTemplate;
      protected readonly ThingRepository $thingRepository;

+     protected function initializeListAction(): void
+     {
+         $this->assertAllowedHttpMethod($this->request, 'GET');
+     }
+
      public function listAction(): ResponseInterface
      {
          $this->moduleTemplate->assignMultiple([
              'things' => $this->thingRepository->findAll(),
          ]);
          return $this->moduleTemplate->renderResponse('ExtbaseModule/List');
      }

+     protected function initializeDeleteAction(): void
+     {
+         $this->assertAllowedHttpMethod($this->request, 'POST');
+     }
+
      public function deleteAction(Thing $thing): ResponseInterface
      {
          $this->thingRepository->remove($thing);
          return $this->redirect('list');
      }
  }
```

#### Template example {#backend-modules-security-cross-site-request-asserting-http-2-template}

In the revised template, `POST`-based form buttons are used
instead of `GET` action links for delete actions:

****Revised** EXT:demo/Resources/Private/Templates/ExtbaseModule/List.fluid.html**

```diff
  <ul>
  <f:for each="{things}" as="thing">
      <li>
          {thing.name}:
-         <f:link.action
-             name="delete" controller="Module"
-             arguments="{thing: thing}"
-             class="btn btn-default">delete</f:link.action>
+         <f:form.button
+             name="thing" value="{thing.uid}"
+             type="submit" form="demo-module-form-delete-action"
+             class="btn btn-default">delete</f:form.button>
      </li>
  </f:for>
  </ul>
+ <f:form
+     action="delete" controller="Module" method="post"
+     id="demo-module-form-delete-action" class="hidden" />
```

> [!TIP]
> **Hint**
>
> The `<button form="identifier">` references the `<form id="identifier">`
> element. This allows multiple `<button>` elements to be used with a single
> `<form>`.
