Skip to content

Commit 4e867b7

Browse files
eliashaeusslerohader
authored andcommitted
[SECURITY] Inherit access to module-related AJAX routes from modules
Several AJAX routes are bound to specific backend modules. While backend modules have proper authorization checks in place, AJAX routes are open to any authenticated backend user. This patch introduces a new config option `inheritAccessFromModule` for AJAX routes which aims to close this gap. It allows to limit access to a specific AJAX route by inheriting access permissions from the given backend module. This is done for all AJAX routes which are used exclusively in specific backend modules. For example, the AJAX route for ext:recycler is now bound to the ext:recycler backend module, inheriting access permissions for this specific route from the given backend module permissions defined in the appropriate be_users / be_groups records. Resolves: #106983 Releases: main, 13.4, 12.4 Change-Id: I8ccaa28468945bc8c7e4fb7e7806ae208e4a46ab Security-Bulletin: TYPO3-CORE-SA-2025-021 Security-References: CVE-2025-59017 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/90631 Tested-by: Oliver Hader <oliver.hader@typo3.org> Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
1 parent 97ed283 commit 4e867b7

File tree

9 files changed

+115
-12
lines changed

9 files changed

+115
-12
lines changed

typo3/sysext/backend/Classes/Middleware/BackendModuleValidator.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use TYPO3\CMS\Backend\Routing\UriBuilder;
3232
use TYPO3\CMS\Backend\Utility\BackendUtility;
3333
use TYPO3\CMS\Core\Http\RedirectResponse;
34+
use TYPO3\CMS\Core\Http\Response;
3435
use TYPO3\CMS\Core\Localization\LanguageService;
3536
use TYPO3\CMS\Core\Messaging\FlashMessage;
3637
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
@@ -68,10 +69,19 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
6869
$inaccessibleSubModule = null;
6970
$ensureToPersistUserSettings = false;
7071
$backendUser = $GLOBALS['BE_USER'] ?? null;
71-
if (!$backendUser
72-
|| !$route->hasOption('module')
73-
|| !(($module = $route->getOption('module')) instanceof ModuleInterface)
74-
) {
72+
73+
if (!$backendUser) {
74+
return $handler->handle($request);
75+
}
76+
77+
// Exit if access to module was denied using module access inheritance check
78+
$inheritAccessFromModule = $route->getOption('inheritAccessFromModule');
79+
if ($inheritAccessFromModule !== null && !$this->moduleProvider->accessGranted($inheritAccessFromModule, $backendUser)) {
80+
return new Response(null, 403);
81+
}
82+
83+
$module = $route->getOption('module');
84+
if (!$module instanceof ModuleInterface) {
7585
return $handler->handle($request);
7686
}
7787

typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
'path' => '/resource/rename',
1818
'methods' => ['POST'],
1919
'target' => Controller\Resource\ResourceController::class . '::renameResourceAction',
20+
'inheritAccessFromModule' => 'media_management',
2021
],
2122

2223
// Link resource
@@ -30,12 +31,14 @@
3031
'file_process' => [
3132
'path' => '/file/process',
3233
'target' => Controller\File\FileController::class . '::processAjaxRequest',
34+
'inheritAccessFromModule' => 'media_management',
3335
],
3436

3537
// Check if file exists
3638
'file_exists' => [
3739
'path' => '/file/exists',
3840
'target' => Controller\File\FileController::class . '::fileExistsInFolderAction',
41+
'inheritAccessFromModule' => 'media_management',
3942
],
4043

4144
// Get details of a file reference in FormEngine
@@ -93,6 +96,7 @@
9396
'site_configuration_inline_create' => [
9497
'path' => '/siteconfiguration/inline/create',
9598
'target' => Controller\SiteInlineAjaxController::class . '::newInlineChildAction',
99+
'inheritAccessFromModule' => 'site_configuration',
96100
],
97101

98102
// Validate slug input
@@ -105,6 +109,7 @@
105109
'site_configuration_inline_details' => [
106110
'path' => '/siteconfiguration/inline/details',
107111
'target' => Controller\SiteInlineAjaxController::class . '::openInlineChildAction',
112+
'inheritAccessFromModule' => 'site_configuration',
108113
],
109114

110115
// Add a flex form section container
@@ -353,18 +358,21 @@
353358
'page_languages' => [
354359
'path' => '/records/localize/get-languages',
355360
'target' => Controller\Page\LocalizationController::class . '::getUsedLanguagesInPage',
361+
'inheritAccessFromModule' => 'web_layout',
356362
],
357363

358364
// Get summary of records to localize
359365
'records_localize_summary' => [
360366
'path' => '/records/localize/summary',
361367
'target' => Controller\Page\LocalizationController::class . '::getRecordLocalizeSummary',
368+
'inheritAccessFromModule' => 'web_layout',
362369
],
363370

364371
// Localize the records
365372
'records_localize' => [
366373
'path' => '/records/localize',
367374
'target' => Controller\Page\LocalizationController::class . '::localizeRecords',
375+
'inheritAccessFromModule' => 'web_layout',
368376
],
369377

370378
// column selector
@@ -407,6 +415,7 @@
407415
'access' => 'systemMaintainer',
408416
'path' => '/security/csp/control',
409417
'target' => \TYPO3\CMS\Backend\Security\ContentSecurityPolicy\CspAjaxController::class . '::handleRequest',
418+
'inheritAccessFromModule' => 'tools_csp',
410419
],
411420

412421
'sudo_mode_control' => [
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
be_users
2-
,uid,pid,tstamp,username,password,admin,disable,starttime,endtime,options,crdate,workspace_perms,deleted,TSconfig,lastlogin,workspace_id,usergroup,mfa
3-
,1,0,1366642540,admin,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,1,0,0,0,0,1366642540,1,0,,1371033743,0,,
4-
,2,0,1366642540,editor,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,0,0,0,0,3,1366642540,1,0,,1371033743,0,1,
5-
,3,0,1366642540,editor_with_groups,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,0,0,0,0,3,1366642540,1,0,"test.default.userValue = from_user_3",1371033743,0,"1,2,4,6",
6-
,4,0,1366642540,mfa_user,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,0,0,0,0,3,1366642540,1,0,,1371033743,0,,"{""totp"":{""secret"":""KRMVATZTJFZUC53FONXW2ZJB"",""active"":true,""attempts"":2}}"
7-
,5,0,1366642540,mfa_admin_locked,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,1,0,0,0,3,1366642540,1,0,,1371033743,0,,"{""totp"":{""secret"":""KRMVATZTJFZUC53FONXW2ZJB"",""active"":true,""attempts"":2},""recovery-codes"":{""active"":true,""attempts"":3,""codes"":[]}}"
2+
,uid,pid,tstamp,username,password,admin,disable,starttime,endtime,options,crdate,workspace_perms,deleted,TSconfig,lastlogin,workspace_id,usergroup,mfa,userMods
3+
,1,0,1366642540,admin,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,1,0,0,0,0,1366642540,1,0,,1371033743,0,,,
4+
,2,0,1366642540,editor,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,0,0,0,0,3,1366642540,1,0,,1371033743,0,1,,
5+
,3,0,1366642540,editor_with_groups,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,0,0,0,0,3,1366642540,1,0,"test.default.userValue = from_user_3",1371033743,0,"1,2,4,6",,"web_layout"
6+
,4,0,1366642540,mfa_user,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,0,0,0,0,3,1366642540,1,0,,1371033743,0,,"{""totp"":{""secret"":""KRMVATZTJFZUC53FONXW2ZJB"",""active"":true,""attempts"":2}}",
7+
,5,0,1366642540,mfa_admin_locked,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,1,0,0,0,3,1366642540,1,0,,1371033743,0,,"{""totp"":{""secret"":""KRMVATZTJFZUC53FONXW2ZJB"",""active"":true,""attempts"":2},""recovery-codes"":{""active"":true,""attempts"":3,""codes"":[]}}",
88
,6,0,1366642540,editor with typoscript,$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1,0,0,0,0,3,1366642540,1,0,"options.impexp.enableImportForNonAdminUser = 1
9-
options.impexp.enableExportForNonAdminUser = 1",1371033743,0,1,
9+
options.impexp.enableExportForNonAdminUser = 1",1371033743,0,1,,

typo3/sysext/backend/Tests/Functional/Middleware/BackendModuleValidatorTest.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ protected function setUp(): void
4747
{
4848
parent::setUp();
4949

50-
$this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
50+
$this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users_core.csv');
5151
$backendUser = $this->setUpBackendUser(1);
5252
$GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser);
5353

@@ -72,6 +72,36 @@ public function handle(ServerRequestInterface $request): ResponseInterface
7272
};
7373
}
7474

75+
#[Test]
76+
public function processReturnsForbiddenResponseIfModuleInheritanceAccessCheckFails(): void
77+
{
78+
$this->setUpBackendUser(2);
79+
80+
$GLOBALS['TYPO3_REQUEST'] = $request = $this->request->withAttribute(
81+
'route',
82+
new Route('/some/route', ['inheritAccessFromModule' => 'web_layout']),
83+
);
84+
85+
$response = $this->subject->process($request, $this->requestHandler);
86+
87+
self::assertSame(403, $response->getStatusCode());
88+
}
89+
90+
#[Test]
91+
public function processReturnsOkResponseIfModuleInheritanceAccessCheckIsSuccessful(): void
92+
{
93+
$this->setUpBackendUser(3);
94+
95+
$GLOBALS['TYPO3_REQUEST'] = $request = $this->request->withAttribute(
96+
'route',
97+
new Route('/some/route', ['inheritAccessFromModule' => 'web_layout']),
98+
);
99+
100+
$response = $this->subject->process($request, $this->requestHandler);
101+
102+
self::assertSame(200, $response->getStatusCode());
103+
}
104+
75105
#[Test]
76106
public function moduleIsAddedToRequest(): void
77107
{

typo3/sysext/beuser/Configuration/Backend/AjaxRoutes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
'user_access_permissions' => [
99
'path' => '/users/access/permissions',
1010
'target' => \TYPO3\CMS\Beuser\Controller\PermissionController::class . '::handleAjaxRequest',
11+
'inheritAccessFromModule' => 'permissions_pages',
1112
],
1213
];
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.. include:: /Includes.rst.txt
2+
3+
.. _important-106983-1750962567:
4+
5+
==================================================================
6+
Important: #106983 - Hardened access to module-related AJAX routes
7+
==================================================================
8+
9+
See :issue:`106983`
10+
11+
Description
12+
===========
13+
14+
AJAX routes which are exclusively used in a specific backend module can now be
15+
configured to inherit access from the respective module. A new configuration
16+
option :php:`inheritAccessFromModule` is introduced to control this behavior.
17+
It is already added to several existing AJAX routes shipped by TYPO3 core.
18+
19+
Requests to routes with an appropriate access check in place will result in a
20+
403 response if the current backend user lacks required permissions.
21+
22+
Example configuration
23+
=====================
24+
25+
In the following example, the `mymodule_myroute` AJAX route inherits access
26+
checks from the `mymodule` backend module:
27+
28+
.. code-block:: php
29+
:caption: EXT:my_extension/Configuration/Backend/AjaxRoutes.php
30+
31+
return [
32+
'mymodule_myroute' => [
33+
'path' => '/mymodule/myroute',
34+
'target' => \MyVendor\MyExtension\Controller\MySpecialController::class . '::mySpecialAction',
35+
'inheritAccessFromModule' => 'mymodule',
36+
],
37+
];
38+
39+
.. index:: Backend

typo3/sysext/dashboard/Configuration/Backend/AjaxRoutes.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,66 +8,78 @@
88
'path' => '/dashboard/dashboards/get',
99
'target' => DashboardAjaxController::class . '::getDashboards',
1010
'methods' => ['GET'],
11+
'inheritAccessFromModule' => 'dashboard',
1112
],
1213
'dashboard_dashboard_add' => [
1314
'path' => '/dashboard/dashboard/add',
1415
'target' => DashboardAjaxController::class . '::addDashboard',
1516
'methods' => ['POST'],
17+
'inheritAccessFromModule' => 'dashboard',
1618
],
1719
'dashboard_dashboard_edit' => [
1820
'path' => '/dashboard/dashboard/edit',
1921
'target' => DashboardAjaxController::class . '::editDashboard',
2022
'methods' => ['POST'],
23+
'inheritAccessFromModule' => 'dashboard',
2124
],
2225
'dashboard_dashboard_update' => [
2326
'path' => '/dashboard/dashboard/update',
2427
'target' => DashboardAjaxController::class . '::updateDashboard',
2528
'methods' => ['POST'],
29+
'inheritAccessFromModule' => 'dashboard',
2630
],
2731
'dashboard_dashboard_delete' => [
2832
'path' => '/dashboard/dashboard/delete',
2933
'target' => DashboardAjaxController::class . '::deleteDashboard',
3034
'methods' => ['POST'],
35+
'inheritAccessFromModule' => 'dashboard',
3136
],
3237

3338
// Presets
3439
'dashboard_presets_get' => [
3540
'path' => '/dashboard/presets/get',
3641
'target' => DashboardAjaxController::class . '::getPresets',
3742
'methods' => ['GET'],
43+
'inheritAccessFromModule' => 'dashboard',
3844
],
3945

4046
// Categories
4147
'dashboard_categories_get' => [
4248
'path' => '/dashboard/categories/get',
4349
'target' => DashboardAjaxController::class . '::getCategories',
4450
'methods' => ['GET'],
51+
'inheritAccessFromModule' => 'dashboard',
4552
],
4653

4754
// Widgets
4855
'dashboard_widget_get' => [
4956
'path' => '/dashboard/widget/get',
5057
'target' => DashboardAjaxController::class . '::getWidget',
5158
'methods' => ['GET'],
59+
'inheritAccessFromModule' => 'dashboard',
5260
],
5361
'dashboard_widget_add' => [
5462
'path' => '/dashboard/widget/add',
5563
'target' => DashboardAjaxController::class . '::addWidget',
5664
'methods' => ['POST'],
65+
'inheritAccessFromModule' => 'dashboard',
5766
],
5867
'dashboard_widget_remove' => [
5968
'path' => '/dashboard/widget/remove',
6069
'target' => DashboardAjaxController::class . '::removeWidget',
6170
'methods' => ['POST'],
71+
'inheritAccessFromModule' => 'dashboard',
6272
],
6373
'dashboard_widget_settings_get' => [
6474
'path' => '/dashboard/widget/settings/get',
6575
'target' => DashboardAjaxController::class . '::getWidgetSettings',
6676
'methods' => ['GET'],
77+
'inheritAccessFromModule' => 'dashboard',
6778
],
6879
'dashboard_widget_settings_update' => [
6980
'path' => '/dashboard/widget/settings/update',
7081
'target' => DashboardAjaxController::class . '::updateWidgetSettings',
7182
'methods' => ['POST'],
83+
'inheritAccessFromModule' => 'dashboard',
7284
],
7385
];

typo3/sysext/recycler/Configuration/Backend/AjaxRoutes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
'recycler' => [
99
'path' => '/recycler',
1010
'target' => \TYPO3\CMS\Recycler\Controller\RecyclerAjaxController::class . '::dispatch',
11+
'inheritAccessFromModule' => 'recycler',
1112
],
1213
];

typo3/sysext/workspaces/Configuration/Backend/AjaxRoutes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
'workspace_dispatch' => [
1313
'path' => '/workspace/dispatch',
1414
'target' => \TYPO3\CMS\Workspaces\Controller\WorkspacesAjaxController::class . '::dispatch',
15+
'inheritAccessFromModule' => 'workspaces_admin',
1516
],
1617
];

0 commit comments

Comments
 (0)