Skip to content

Commit c2e5dbd

Browse files
committed
[SECURITY] Enforce HTTP method assertions for backend modules
Resolves: #104456 Releases: main, 13.4, 12.4 Change-Id: Ic679584a343b6d35e81325a03148b0cff81f1d27 Security-Bulletin: TYPO3-CORE-SA-2025-003 Security-Bulletin: TYPO3-CORE-SA-2025-004 Security-Bulletin: TYPO3-CORE-SA-2025-005 Security-Bulletin: TYPO3-CORE-SA-2025-006 Security-Bulletin: TYPO3-CORE-SA-2025-007 Security-Bulletin: TYPO3-CORE-SA-2025-008 Security-References: CVE-2024-55893 Security-References: CVE-2024-55894 Security-References: CVE-2024-55920 Security-References: CVE-2024-55921 Security-References: CVE-2024-55922 Security-References: CVE-2024-55923 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/87744 Tested-by: Oliver Hader <oliver.hader@typo3.org> Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
1 parent 29d5197 commit c2e5dbd

File tree

8 files changed

+54
-40
lines changed

8 files changed

+54
-40
lines changed

Classes/Controller/DashboardController.php

+15-5
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
use Psr\Http\Message\ResponseInterface;
2121
use Psr\Http\Message\ServerRequestInterface;
2222
use TYPO3\CMS\Backend\Attribute\AsController;
23+
use TYPO3\CMS\Backend\Domain\Model\Element\ImmediateActionElement;
2324
use TYPO3\CMS\Backend\Routing\UriBuilder;
2425
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
2526
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
27+
use TYPO3\CMS\Core\Http\AllowedMethodsTrait;
28+
use TYPO3\CMS\Core\Http\HtmlResponse;
2629
use TYPO3\CMS\Core\Http\RedirectResponse;
2730
use TYPO3\CMS\Core\Localization\LanguageService;
2831
use TYPO3\CMS\Core\Page\PageRenderer;
@@ -40,6 +43,8 @@
4043
#[AsController]
4144
class DashboardController
4245
{
46+
use AllowedMethodsTrait;
47+
4348
protected Dashboard $currentDashboard;
4449

4550
public function __construct(
@@ -90,6 +95,7 @@ protected function mainAction(ServerRequestInterface $request): ResponseInterfac
9095

9196
protected function configureDashboardAction(ServerRequestInterface $request): ResponseInterface
9297
{
98+
$this->assertAllowedHttpMethod($request, 'POST');
9399
$parameters = $request->getParsedBody();
94100
$currentDashboard = $parameters['currentDashboard'] ?? '';
95101
$route = $this->uriBuilder->buildUriFromRoute('dashboard', ['action' => 'main'], UriBuilder::ABSOLUTE_URL);
@@ -101,13 +107,15 @@ protected function configureDashboardAction(ServerRequestInterface $request): Re
101107

102108
protected function setActiveDashboardAction(ServerRequestInterface $request): ResponseInterface
103109
{
104-
$this->saveCurrentDashboard((string)($request->getQueryParams()['currentDashboard'] ?? ''));
110+
$this->assertAllowedHttpMethod($request, 'POST');
111+
$this->saveCurrentDashboard((string)($request->getParsedBody()['currentDashboard'] ?? ''));
105112
$route = $this->uriBuilder->buildUriFromRoute('dashboard', ['action' => 'main']);
106113
return new RedirectResponse($route);
107114
}
108115

109116
protected function addDashboardAction(ServerRequestInterface $request): ResponseInterface
110117
{
118+
$this->assertAllowedHttpMethod($request, 'POST');
111119
$parameters = $request->getParsedBody();
112120
$dashboardIdentifier = (string)($parameters['dashboard'] ?? '');
113121
$dashboardPreset = $this->dashboardPresetRepository->getDashboardPresets()[$dashboardIdentifier] ?? null;
@@ -124,14 +132,16 @@ protected function addDashboardAction(ServerRequestInterface $request): Response
124132
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('dashboard', ['action' => 'main']));
125133
}
126134

127-
protected function deleteDashboardAction(): ResponseInterface
135+
protected function deleteDashboardAction(ServerRequestInterface $request): ResponseInterface
128136
{
137+
$this->assertAllowedHttpMethod($request, 'POST');
129138
$this->dashboardRepository->delete($this->currentDashboard);
130139
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('dashboard', ['action' => 'main']));
131140
}
132141

133142
protected function addWidgetAction(ServerRequestInterface $request): ResponseInterface
134143
{
144+
$this->assertAllowedHttpMethod($request, 'POST');
135145
$widgetKey = (string)($request->getQueryParams()['widget'] ?? '');
136146
if ($widgetKey === '') {
137147
throw new RequiredArgumentMissingException('Argument "widget" not set.', 1624436360);
@@ -140,13 +150,13 @@ protected function addWidgetAction(ServerRequestInterface $request): ResponseInt
140150
$hash = sha1($widgetKey . '-' . time());
141151
$widgets[$hash] = ['identifier' => $widgetKey];
142152
$this->dashboardRepository->updateWidgetConfig($this->currentDashboard, $widgets);
143-
$route = $this->uriBuilder->buildUriFromRoute('dashboard', ['action' => 'main']);
144-
return new RedirectResponse($route);
153+
return new HtmlResponse((string)ImmediateActionElement::dispatchCustomEvent('typo3.dashboard.addWidgetDone'));
145154
}
146155

147156
protected function removeWidgetAction(ServerRequestInterface $request): ResponseInterface
148157
{
149-
$parameters = $request->getQueryParams();
158+
$this->assertAllowedHttpMethod($request, 'POST');
159+
$parameters = $request->getParsedBody();
150160
$widgetHash = $parameters['widgetHash'] ?? '';
151161
$widgets = $this->currentDashboard->getWidgetConfig();
152162
if ($widgetHash !== '' && array_key_exists($widgetHash, $widgets)) {

Classes/WidgetGroupInitializationService.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ public function buildWidgetGroupsConfiguration(): array
5151
'label' => $this->getLanguageService()->sL($widgetConfiguration->getTitle()),
5252
'description' => $this->getLanguageService()->sL($widgetConfiguration->getDescription()),
5353
'url' => (string)$this->uriBuilder->buildUriFromRoute('dashboard', ['action' => 'addWidget', 'widget' => $widgetIdentifier]),
54-
'requestType' => 'location',
54+
'requestType' => 'ajax',
55+
'defaultValues' => [],
56+
'saveAndClose' => true,
5557
];
5658
}
5759

Resources/Private/Layouts/Module.html

+20-18
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ <h1 class="visually-hidden">
2020
<ul class="dashboard-tabs-menu" role="menubar">
2121
<f:for each="{availableDashboards}" as="dashboard" key="dashboardKey">
2222
<li class="dashboard-tabs-menuitem" role="menuitem">
23-
<f:be.link
24-
route="dashboard"
25-
parameters="{action: 'setActiveDashboard', currentDashboard: dashboardKey}"
26-
class="dashboard-tab {f:if(condition: '{dashboardKey} == {currentDashboard.identifier}', then: 'dashboard-tab--active')}"
27-
>
23+
<button
24+
name="currentDashboard"
25+
value="{dashboardKey}"
26+
form="form-set-active-dashboard"
27+
class="dashboard-tab {f:if(condition: '{dashboardKey} == {currentDashboard.identifier}', then: 'dashboard-tab--active')}">
2828
{dashboard.title}
29-
</f:be.link>
29+
</button>
3030
</li>
3131
</f:for>
3232
</ul>
@@ -42,6 +42,7 @@ <h1 class="visually-hidden">
4242
>
4343
<core:icon identifier="actions-plus" alternativeMarkupIdentifier="inline" /><span class="visually-hidden"><f:translate key="dashboard.add" extensionName="dashboard"/></span>
4444
</a>
45+
<form id="form-set-active-dashboard" class="hidden" action="{f:be.uri(route: 'dashboard', parameters: '{action: \'setActiveDashboard\'}')}" method="post"></form>
4546
</div>
4647

4748
<div class="dashboard-configuration btn-toolbar" role="toolbar">
@@ -57,18 +58,19 @@ <h1 class="visually-hidden">
5758
>
5859
<core:icon identifier="actions-cog" alternativeMarkupIdentifier="inline" /><span class="visually-hidden"><f:translate key="dashboard.configure" extensionName="dashboard"/></span>
5960
</a>
60-
<a
61-
href="{deleteDashboardUri}"
62-
class="js-dashboard-delete btn btn-default btn-sm"
63-
title="{f:translate(key: 'dashboard.delete', extensionName: 'dashboard')}"
64-
data-modal-title="{f:translate(key: 'dashboard.delete', extensionName: 'dashboard')}"
65-
data-modal-question="{f:translate(key: 'dashboard.delete.sure', extensionName: 'dashboard')}"
66-
data-modal-ok="{f:translate(key: 'dashboard.delete.ok', extensionName: 'dashboard')}"
67-
data-modal-cancel="{f:translate(key: 'dashboard.delete.cancel', extensionName: 'dashboard')}"
68-
role="button"
69-
>
70-
<core:icon identifier="actions-delete" alternativeMarkupIdentifier="inline" /><span class="visually-hidden"><f:translate key="dashboard.delete" extensionName="dashboard"/></span>
71-
</a>
61+
<form action="{deleteDashboardUri}" method="post" class="form-inline">
62+
<button
63+
class="js-dashboard-delete btn btn-default btn-sm"
64+
title="{f:translate(key: 'dashboard.delete', extensionName: 'dashboard')}"
65+
data-modal-title="{f:translate(key: 'dashboard.delete', extensionName: 'dashboard')}"
66+
data-modal-question="{f:translate(key: 'dashboard.delete.sure', extensionName: 'dashboard')}"
67+
data-modal-ok="{f:translate(key: 'dashboard.delete.ok', extensionName: 'dashboard')}"
68+
data-modal-cancel="{f:translate(key: 'dashboard.delete.cancel', extensionName: 'dashboard')}"
69+
role="button"
70+
>
71+
<core:icon identifier="actions-delete" alternativeMarkupIdentifier="inline" /><span class="visually-hidden"><f:translate key="dashboard.delete" extensionName="dashboard"/></span>
72+
</button>
73+
</form>
7274
</div>
7375
</div>
7476
</div>

Resources/Private/Templates/Dashboard/Main.html

+12-12
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,18 @@
4141
<core:icon identifier="actions-refresh" alternativeMarkupIdentifier="inline"/>
4242
</typo3-dashboard-widget-refresh>
4343
</f:if>
44-
<f:be.link
45-
class="widget-action widget-action-remove js-dashboard-remove-widget"
46-
route="dashboard" parameters="{action: 'removeWidget', widgetHash: widgetHash}"
47-
data="{
48-
modal-title: '{f:translate(key: \'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.remove.confirm.title\')}',
49-
modal-question: '{f:translate(key: \'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.remove.confirm.message\')}',
50-
modal-ok: '{f:translate(key: \'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.remove.button.ok\')}',
51-
modal-cancel: '{f:translate(key: \'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.remove.button.close\')}'
52-
}"
53-
>
54-
<core:icon identifier="actions-delete" alternativeMarkupIdentifier="inline"/>
55-
</f:be.link>
44+
<form action="{f:be.uri(route: 'dashboard', parameters: '{action: \'removeWidget\'}')}" method="post" class="form-inline">
45+
<input type="hidden" name="widgetHash" value="{widgetHash}">
46+
<button
47+
class="widget-action widget-action-remove js-dashboard-remove-widget"
48+
data-modal-title="{f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.remove.confirm.title')}"
49+
data-modal-question="{f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.remove.confirm.message')}"
50+
data-modal-ok="{f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.remove.button.ok')}"
51+
data-modal-cancel="{f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.remove.button.close')}"
52+
>
53+
<core:icon identifier="actions-delete" alternativeMarkupIdentifier="inline"/>
54+
</button>
55+
</form>
5656
</div>
5757
</div>
5858
</div>

Resources/Public/Css/dashboard.css

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Resources/Public/JavaScript/dashboard-delete.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
*
1111
* The TYPO3 project - inspiring people to share!
1212
*/
13-
import Modal from"@typo3/backend/modal.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import RegularEvent from"@typo3/core/event/regular-event.js";class DashboardDelete{constructor(){this.selector=".js-dashboard-delete",this.initialize()}initialize(){new RegularEvent("click",(function(e){e.preventDefault();Modal.confirm(this.dataset.modalTitle,this.dataset.modalQuestion,SeverityEnum.warning,[{text:this.dataset.modalCancel,active:!0,btnClass:"btn-default",name:"cancel"},{text:this.dataset.modalOk,btnClass:"btn-warning",name:"delete"}]).addEventListener("button.clicked",(e=>{"delete"===e.target.getAttribute("name")&&(window.location.href=this.getAttribute("href")),Modal.dismiss()}))})).delegateTo(document,this.selector)}}export default new DashboardDelete;
13+
import Modal from"@typo3/backend/modal.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import RegularEvent from"@typo3/core/event/regular-event.js";class DashboardDelete{constructor(){this.selector=".js-dashboard-delete",this.initialize()}initialize(){new RegularEvent("click",(function(e){e.preventDefault();const t=this.form;Modal.confirm(this.dataset.modalTitle,this.dataset.modalQuestion,SeverityEnum.warning,[{text:this.dataset.modalCancel,active:!0,btnClass:"btn-default",name:"cancel"},{text:this.dataset.modalOk,btnClass:"btn-warning",name:"delete"}]).addEventListener("button.clicked",(e=>{"delete"===e.target.getAttribute("name")&&t.requestSubmit(),Modal.dismiss()}))})).delegateTo(document,this.selector)}}export default new DashboardDelete;

Resources/Public/JavaScript/widget-remover.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
*
1111
* The TYPO3 project - inspiring people to share!
1212
*/
13-
import Modal from"@typo3/backend/modal.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import RegularEvent from"@typo3/core/event/regular-event.js";class WidgetRemover{constructor(){this.selector=".js-dashboard-remove-widget",this.initialize()}initialize(){new RegularEvent("click",(function(e){e.preventDefault();const t=Modal.confirm(this.dataset.modalTitle,this.dataset.modalQuestion,SeverityEnum.warning,[{text:this.dataset.modalCancel,active:!0,btnClass:"btn-default",name:"cancel"},{text:this.dataset.modalOk,btnClass:"btn-warning",name:"delete"}]);t.addEventListener("button.clicked",(e=>{"delete"===e.target.getAttribute("name")&&(window.location.href=this.getAttribute("href")),t.hideModal()}))})).delegateTo(document,this.selector)}}export default new WidgetRemover;
13+
import Modal from"@typo3/backend/modal.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import RegularEvent from"@typo3/core/event/regular-event.js";class WidgetRemover{constructor(){this.selector=".js-dashboard-remove-widget",this.initialize()}initialize(){new RegularEvent("click",(function(e){e.preventDefault();const t=this.form,a=Modal.confirm(this.dataset.modalTitle,this.dataset.modalQuestion,SeverityEnum.warning,[{text:this.dataset.modalCancel,active:!0,btnClass:"btn-default",name:"cancel"},{text:this.dataset.modalOk,btnClass:"btn-warning",name:"delete"}]);a.addEventListener("button.clicked",(e=>{"delete"===e.target.getAttribute("name")&&t.requestSubmit(),a.hideModal()}))})).delegateTo(document,this.selector)}}export default new WidgetRemover;

Resources/Public/JavaScript/widget-selector.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)