TYPO3 CMS Allows Broken Access Control in Edit Document Controller
Description
By exploiting the defVals parameter, attackers could bypass field‑level access checks during record creation in the TYPO3 backend. This gave them the ability to insert arbitrary data into prohibited exclude fields of a database table for which the user already has write permission for a reduced set of fields. This issue affects TYPO3 CMS versions 10.0.0-10.4.54, 11.0.0-11.5.48, 12.0.0-12.4.40, 13.0.0-13.4.22 and 14.0.0-14.0.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
typo3/cms-backendPackagist | >= 14.0.0, < 14.0.2 | 14.0.2 |
typo3/cms-backendPackagist | >= 13.0.0, < 13.4.23 | 13.4.23 |
typo3/cms-backendPackagist | >= 12.0.0, < 12.4.41 | 12.4.41 |
typo3/cms-backendPackagist | >= 11.0.0, < 11.5.49 | 11.5.49 |
typo3/cms-backendPackagist | >= 10.0.0, < 10.4.55 | 10.4.55 |
Affected products
1Patches
3ac3f792bd5ab[SECURITY] Ensure defVals adhere to permissions checks
6 files changed · +284 −10
typo3/sysext/backend/Classes/Controller/EditDocumentController.php+11 −1 modified@@ -523,7 +523,17 @@ protected function processData(ModuleTemplate $view, ServerRequestInterface $req $dataHandler->setControl($parsedBody['control'] ?? []); // Set default values fetched previously from GET / POST vars - $dataHandler->defaultValues = $this->defVals ?? []; + if (is_array($dataMap)) { + foreach ($dataMap as $tableName => $records) { + if (is_array($this->defVals[$tableName] ?? null)) { + foreach ($records as $uid => $_) { + if (str_contains((string)$uid, 'NEW')) { + $dataMap[$tableName][$uid] = array_merge($this->defVals[$tableName], $dataMap[$tableName][$uid]); + } + } + } + } + } // Load DataHandler with data $dataHandler->start($dataMap, $dataHandlerIncomingCommandMap);
typo3/sysext/backend/Tests/Functional/Controller/EditDocumentControllerAsUserTest.php+256 −0 added@@ -0,0 +1,256 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Tests\Functional\Controller; + +use PHPUnit\Framework\Attributes\Test; +use TYPO3\CMS\Backend\Controller\EditDocumentController; +use TYPO3\CMS\Backend\Routing\Route; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\Http\NormalizedParams; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Localization\LanguageServiceFactory; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +final class EditDocumentControllerAsUserTest extends FunctionalTestCase +{ + protected EditDocumentController $subject; + + protected NormalizedParams $normalizedParams; + + /** + * Sets up this test case. + */ + protected function setUp(): void + { + parent::setUp(); + + $this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/tt_content.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_groups_with_editor.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users_with_editor.csv'); + // editor + $backendUser = $this->setUpBackendUser(9); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + + $this->subject = $this->get(EditDocumentController::class); + $this->normalizedParams = new NormalizedParams([], [], '', ''); + } + + #[Test] + public function processedDataTakesOverDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + ]; + + $queryParams = $this->getQueryParams($defaultValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$defaultValues['colPos'], $defaultValues['CType']], + [$newRecord['colPos'], $newRecord['CType']] + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataDoesNotOverridePostWithDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + ]; + + $queryParams = $this->getQueryParams($defaultValues); + $parsedBody = $this->getParsedBody(['colPos' => 0, 'CType' => 'text']); + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [0, 'text'], + [$newRecord['colPos'], $newRecord['CType']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataOmitsProhibitedDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + 'subheader' => 'disallowed', + 'header_position' => 'center', + ]; + + $queryParams = $this->getQueryParams($defaultValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$defaultValues['colPos'], $defaultValues['CType'], '', 'center'], + [$newRecord['colPos'], $newRecord['CType'], $newRecord['subheader'], $newRecord['header_position']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataUsesOverrideValuesOnEmptyDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + 'subheader' => 'disallowed', + 'header_position' => 'center', + ]; + $overrideValues = [ + 'colPos' => 456, + 'CType' => 'image', + 'subheader' => 'allowed', + 'header_position' => 'left', + ]; + + $queryParams = $this->getQueryParams($defaultValues, $overrideValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$defaultValues['colPos'], $defaultValues['CType'], '', 'center'], + [$newRecord['colPos'], $newRecord['CType'], $newRecord['subheader'], $newRecord['header_position']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataPrioritizesDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $overrideValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + 'subheader' => 'disallowed', + 'header_position' => 'center', + ]; + + $queryParams = $this->getQueryParams([], $overrideValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$overrideValues['colPos'], $overrideValues['CType'], '', 'center'], + [$newRecord['colPos'], $newRecord['CType'], $newRecord['subheader'], $newRecord['header_position']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + protected function getParsedBody(array $additionalData = []): array + { + return [ + 'data' => [ + 'tt_content' => [ + 'NEW123456' => array_replace_recursive([ + 'sys_language_uid' => 0, + 'header' => 'Test header', + 'pid' => 1, + ], $additionalData), + ], + ], + 'doSave' => true, + ]; + } + + protected function getQueryParams(array $defaultValues = [], array $overrideValues = []): array + { + $params = [ + 'edit' => [ + 'tt_content' => [ + -1 => 'new', + ], + ], + ]; + + if ($defaultValues !== []) { + $params['defVals'] = [ + 'tt_content' => $defaultValues, + ]; + } + + if ($overrideValues !== []) { + $params['overrideVals'] = [ + 'tt_content' => $overrideValues, + ]; + } + + return $params; + } +}
typo3/sysext/backend/Tests/Functional/Fixtures/be_groups_with_editor.csv+3 −0 added@@ -0,0 +1,3 @@ +be_groups,,,,,,,, +,uid,pid,title,deleted,db_mountpoints,tables_select,tables_modify,non_exclude_fields,explicit_allowdeny +,9,0,editors,0,1,"pages,tt_content","pages,tt_content","pages:title,tt_content:header,tt_content:CType,tt_content:sys_language_uid,tt_content:header_position","tt_content:CType:text,tt_content:CType:bullets"
typo3/sysext/backend/Tests/Functional/Fixtures/be_users_with_editor.csv+5 −0 added@@ -0,0 +1,5 @@ +"be_users",,,,,,, +,"uid","pid","username","usergroup","deleted","admin","options","TSconfig" +,1,0,"admin",,0,1,0,"" +,9,0,"editor",9,0,0,3,"page.TCEFORM.tt_content.header_position.disabled = 1 +page.TCEFORM.tt_content.colPos.readOnly = 1"
typo3/sysext/backend/Tests/Functional/Fixtures/pages.csv+8 −8 modified@@ -1,12 +1,12 @@ "pages" -,"uid","pid","sorting","title","deleted","perms_everybody","TSconfig" -,1,0,128,"Root",0,15,"some_property=0" -,2,1,128,"Dummy 1-2",0,15,"" -,3,2,128,"Dummy 1-2-3",0,15,"" -,4,3,128,"Dummy 1-2-3-4",0,15,"" -,5,1,256,"Dummy 1-5",0,15,"some_property=5 +,"uid","pid","sorting","title","deleted","perms_groupid","perms_group","perms_everybody","TSconfig" +,1,0,128,"Root",0,9,27,15,"some_property=0" +,2,1,128,"Dummy 1-2",0,9,27,15,"" +,3,2,128,"Dummy 1-2-3",0,9,27,15,"" +,4,3,128,"Dummy 1-2-3-4",0,9,27,15,"" +,5,1,256,"Dummy 1-5",0,9,27,15,"some_property=5 [6 in tree.rootLineIds] some_property=6 [end]" -,6,5,128,"Dummy 1-5-6",0,15,"" -,7,0,256,"Root 2",0,15,"" +,6,5,128,"Dummy 1-5-6",0,9,27,15,"" +,7,0,256,"Root 2",0,9,27,15,""
typo3/sysext/core/Classes/DataHandling/DataHandler.php+1 −1 modified@@ -152,7 +152,7 @@ class DataHandler * If ->setDefaultsFromUserTS is called UserTSconfig default values will overrule existing values in this array * (thus UserTSconfig overrules externally set defaults which overrules TCA defaults) * - * @internal should only be used from within TYPO3 Core + * @internal should only be used from within DataHandler as permission checks do not apply to default values */ public array $defaultValues = [];
fb98378a8fd3[SECURITY] Ensure defVals adhere to permissions checks
6 files changed · +283 −11
typo3/sysext/backend/Classes/Controller/EditDocumentController.php+10 −2 modified@@ -545,8 +545,16 @@ protected function processData(ModuleTemplate $view, ServerRequestInterface $req } // Set default values fetched previously from GET / POST vars - if (is_array($this->defVals) && $this->defVals !== []) { - $tce->defaultValues = array_merge_recursive($this->defVals, $tce->defaultValues); + if (is_array($this->data)) { + foreach ($this->data as $tableName => $records) { + if (is_array($this->defVals[$tableName] ?? null)) { + foreach ($records as $uid => $_) { + if (str_contains((string)$uid, 'NEW')) { + $this->data[$tableName][$uid] = array_merge($this->defVals[$tableName], $this->data[$tableName][$uid]); + } + } + } + } } // Load DataHandler with data
typo3/sysext/backend/Tests/Functional/Controller/EditDocumentControllerAsUserTest.php+256 −0 added@@ -0,0 +1,256 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Tests\Functional\Controller; + +use PHPUnit\Framework\Attributes\Test; +use TYPO3\CMS\Backend\Controller\EditDocumentController; +use TYPO3\CMS\Backend\Routing\Route; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\Http\NormalizedParams; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Localization\LanguageServiceFactory; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +final class EditDocumentControllerAsUserTest extends FunctionalTestCase +{ + protected EditDocumentController $subject; + + protected NormalizedParams $normalizedParams; + + /** + * Sets up this test case. + */ + protected function setUp(): void + { + parent::setUp(); + + $this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/tt_content.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_groups_with_editor.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users_with_editor.csv'); + // editor + $backendUser = $this->setUpBackendUser(9); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + + $this->subject = $this->get(EditDocumentController::class); + $this->normalizedParams = new NormalizedParams([], [], '', ''); + } + + #[Test] + public function processedDataTakesOverDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + ]; + + $queryParams = $this->getQueryParams($defaultValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$defaultValues['colPos'], $defaultValues['CType']], + [$newRecord['colPos'], $newRecord['CType']] + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataDoesNotOverridePostWithDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + ]; + + $queryParams = $this->getQueryParams($defaultValues); + $parsedBody = $this->getParsedBody(['colPos' => 0, 'CType' => 'text']); + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [0, 'text'], + [$newRecord['colPos'], $newRecord['CType']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataOmitsProhibitedDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + 'subheader' => 'disallowed', + 'header_position' => 'center', + ]; + + $queryParams = $this->getQueryParams($defaultValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$defaultValues['colPos'], $defaultValues['CType'], '', 'center'], + [$newRecord['colPos'], $newRecord['CType'], $newRecord['subheader'], $newRecord['header_position']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataUsesOverrideValuesOnEmptyDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + 'subheader' => 'disallowed', + 'header_position' => 'center', + ]; + $overrideValues = [ + 'colPos' => 456, + 'CType' => 'image', + 'subheader' => 'allowed', + 'header_position' => 'left', + ]; + + $queryParams = $this->getQueryParams($defaultValues, $overrideValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$defaultValues['colPos'], $defaultValues['CType'], '', 'center'], + [$newRecord['colPos'], $newRecord['CType'], $newRecord['subheader'], $newRecord['header_position']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataPrioritizesDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $overrideValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + 'subheader' => 'disallowed', + 'header_position' => 'center', + ]; + + $queryParams = $this->getQueryParams([], $overrideValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$overrideValues['colPos'], $overrideValues['CType'], '', 'center'], + [$newRecord['colPos'], $newRecord['CType'], $newRecord['subheader'], $newRecord['header_position']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + protected function getParsedBody(array $additionalData = []): array + { + return [ + 'data' => [ + 'tt_content' => [ + 'NEW123456' => array_replace_recursive([ + 'sys_language_uid' => 0, + 'header' => 'Test header', + 'pid' => 1, + ], $additionalData), + ], + ], + 'doSave' => true, + ]; + } + + protected function getQueryParams(array $defaultValues = [], array $overrideValues = []): array + { + $params = [ + 'edit' => [ + 'tt_content' => [ + -1 => 'new', + ], + ], + ]; + + if ($defaultValues !== []) { + $params['defVals'] = [ + 'tt_content' => $defaultValues, + ]; + } + + if ($overrideValues !== []) { + $params['overrideVals'] = [ + 'tt_content' => $overrideValues, + ]; + } + + return $params; + } +}
typo3/sysext/backend/Tests/Functional/Fixtures/be_groups_with_editor.csv+3 −0 added@@ -0,0 +1,3 @@ +be_groups,,,,,,,, +,uid,pid,title,deleted,db_mountpoints,tables_select,tables_modify,non_exclude_fields,explicit_allowdeny +,9,0,editors,0,1,"pages,tt_content","pages,tt_content","pages:title,tt_content:header,tt_content:CType,tt_content:sys_language_uid,tt_content:header_position","tt_content:CType:text,tt_content:CType:bullets"
typo3/sysext/backend/Tests/Functional/Fixtures/be_users_with_editor.csv+5 −0 added@@ -0,0 +1,5 @@ +"be_users",,,,,,, +,"uid","pid","username","usergroup","deleted","admin","options","TSconfig" +,1,0,"admin",,0,1,0,"" +,9,0,"editor",9,0,0,3,"page.TCEFORM.tt_content.header_position.disabled = 1 +page.TCEFORM.tt_content.colPos.readOnly = 1"
typo3/sysext/backend/Tests/Functional/Fixtures/pages.csv+8 −8 modified@@ -1,12 +1,12 @@ "pages" -,"uid","pid","sorting","title","deleted","perms_everybody","TSconfig" -,1,0,128,"Root",0,15,"some_property=0" -,2,1,128,"Dummy 1-2",0,15,"" -,3,2,128,"Dummy 1-2-3",0,15,"" -,4,3,128,"Dummy 1-2-3-4",0,15,"" -,5,1,256,"Dummy 1-5",0,15,"some_property=5 +,"uid","pid","sorting","title","deleted","perms_groupid","perms_group","perms_everybody","TSconfig" +,1,0,128,"Root",0,9,27,15,"some_property=0" +,2,1,128,"Dummy 1-2",0,9,27,15,"" +,3,2,128,"Dummy 1-2-3",0,9,27,15,"" +,4,3,128,"Dummy 1-2-3-4",0,9,27,15,"" +,5,1,256,"Dummy 1-5",0,9,27,15,"some_property=5 [6 in tree.rootLineIds] some_property=6 [end]" -,6,5,128,"Dummy 1-5-6",0,15,"" -,7,0,256,"Root 2",0,15,"" +,6,5,128,"Dummy 1-5-6",0,9,27,15,"" +,7,0,256,"Root 2",0,9,27,15,""
typo3/sysext/core/Classes/DataHandling/DataHandler.php+1 −1 modified@@ -193,7 +193,7 @@ class DataHandler * If ->setDefaultsFromUserTS is called UserTSconfig default values will overrule existing values in this array * (thus UserTSconfig overrules externally set defaults which overrules TCA defaults) * - * @internal should only be used from within TYPO3 Core + * @internal should only be used from within DataHandler as permission checks do not apply to default values */ public array $defaultValues = [];
cd11a19958d8[SECURITY] Ensure defVals adhere to permissions checks
7 files changed · +284 −12
Build/phpstan/phpstan-baseline.neon+1 −1 modified@@ -51,7 +51,7 @@ parameters: - message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' identifier: function.alreadyNarrowedType - count: 4 + count: 3 path: ../../typo3/sysext/backend/Classes/Controller/EditDocumentController.php -
typo3/sysext/backend/Classes/Controller/EditDocumentController.php+10 −2 modified@@ -535,8 +535,16 @@ protected function processData(ModuleTemplate $view, ServerRequestInterface $req } // Set default values fetched previously from GET / POST vars - if (is_array($this->defVals) && $this->defVals !== [] && is_array($tce->defaultValues)) { - $tce->defaultValues = array_merge_recursive($this->defVals, $tce->defaultValues); + if (is_array($this->data)) { + foreach ($this->data as $tableName => $records) { + if (is_array($this->defVals[$tableName] ?? null)) { + foreach ($records as $uid => $_) { + if (str_contains((string)$uid, 'NEW')) { + $this->data[$tableName][$uid] = array_merge($this->defVals[$tableName], $this->data[$tableName][$uid]); + } + } + } + } } // Load DataHandler with data
typo3/sysext/backend/Tests/Functional/Controller/EditDocumentControllerAsUserTest.php+256 −0 added@@ -0,0 +1,256 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Tests\Functional\Controller; + +use PHPUnit\Framework\Attributes\Test; +use TYPO3\CMS\Backend\Controller\EditDocumentController; +use TYPO3\CMS\Backend\Routing\Route; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\Http\NormalizedParams; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Localization\LanguageServiceFactory; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +final class EditDocumentControllerAsUserTest extends FunctionalTestCase +{ + protected EditDocumentController $subject; + + protected NormalizedParams $normalizedParams; + + /** + * Sets up this test case. + */ + protected function setUp(): void + { + parent::setUp(); + + $this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/tt_content.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_groups_with_editor.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users_with_editor.csv'); + // editor + $backendUser = $this->setUpBackendUser(9); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + + $this->subject = $this->get(EditDocumentController::class); + $this->normalizedParams = new NormalizedParams([], [], '', ''); + } + + #[Test] + public function processedDataTakesOverDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + ]; + + $queryParams = $this->getQueryParams($defaultValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$defaultValues['colPos'], $defaultValues['CType']], + [$newRecord['colPos'], $newRecord['CType']] + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataDoesNotOverridePostWithDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + ]; + + $queryParams = $this->getQueryParams($defaultValues); + $parsedBody = $this->getParsedBody(['colPos' => 0, 'CType' => 'text']); + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [0, 'text'], + [$newRecord['colPos'], $newRecord['CType']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataOmitsProhibitedDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + 'subheader' => 'disallowed', + 'header_position' => 'center', + ]; + + $queryParams = $this->getQueryParams($defaultValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$defaultValues['colPos'], $defaultValues['CType'], '', 'center'], + [$newRecord['colPos'], $newRecord['CType'], $newRecord['subheader'], $newRecord['header_position']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataUsesOverrideValuesOnEmptyDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $defaultValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + 'subheader' => 'disallowed', + 'header_position' => 'center', + ]; + $overrideValues = [ + 'colPos' => 456, + 'CType' => 'image', + 'subheader' => 'allowed', + 'header_position' => 'left', + ]; + + $queryParams = $this->getQueryParams($defaultValues, $overrideValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$defaultValues['colPos'], $defaultValues['CType'], '', 'center'], + [$newRecord['colPos'], $newRecord['CType'], $newRecord['subheader'], $newRecord['header_position']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + #[Test] + public function processedDataPrioritizesDefaultValues(): void + { + $request = (new ServerRequest('https://www.example.com/', 'POST')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $overrideValues = [ + 'colPos' => 123, + 'CType' => 'bullets', + 'subheader' => 'disallowed', + 'header_position' => 'center', + ]; + + $queryParams = $this->getQueryParams([], $overrideValues); + $parsedBody = $this->getParsedBody(); + + $request = $request + ->withAttribute('normalizedParams', $this->normalizedParams) + ->withAttribute('route', new Route('path', ['packageName' => 'typo3/cms-backend'])) + ->withQueryParams($queryParams) + ->withParsedBody($parsedBody); + $GLOBALS['TYPO3_REQUEST'] = $request; + $response = $this->subject->mainAction($request); + + $newRecord = BackendUtility::getRecord('tt_content', 2); + self::assertEquals( + [$overrideValues['colPos'], $overrideValues['CType'], '', 'center'], + [$newRecord['colPos'], $newRecord['CType'], $newRecord['subheader'], $newRecord['header_position']], + ); + // Redirect to GET is applied after processing + self::assertEquals(302, $response->getStatusCode()); + } + + protected function getParsedBody(array $additionalData = []): array + { + return [ + 'data' => [ + 'tt_content' => [ + 'NEW123456' => array_replace_recursive([ + 'sys_language_uid' => 0, + 'header' => 'Test header', + 'pid' => 1, + ], $additionalData), + ], + ], + 'doSave' => true, + ]; + } + + protected function getQueryParams(array $defaultValues = [], array $overrideValues = []): array + { + $params = [ + 'edit' => [ + 'tt_content' => [ + -1 => 'new', + ], + ], + ]; + + if ($defaultValues !== []) { + $params['defVals'] = [ + 'tt_content' => $defaultValues, + ]; + } + + if ($overrideValues !== []) { + $params['overrideVals'] = [ + 'tt_content' => $overrideValues, + ]; + } + + return $params; + } +}
typo3/sysext/backend/Tests/Functional/Fixtures/be_groups_with_editor.csv+3 −0 added@@ -0,0 +1,3 @@ +be_groups,,,,,,,, +,uid,pid,title,deleted,db_mountpoints,tables_select,tables_modify,non_exclude_fields,explicit_allowdeny +,9,0,editors,0,1,"pages,tt_content","pages,tt_content","pages:title,tt_content:header,tt_content:CType,tt_content:sys_language_uid,tt_content:header_position","tt_content:CType:text,tt_content:CType:bullets"
typo3/sysext/backend/Tests/Functional/Fixtures/be_users_with_editor.csv+5 −0 added@@ -0,0 +1,5 @@ +"be_users",,,,,,, +,"uid","pid","username","usergroup","deleted","admin","options","TSconfig" +,1,0,"admin",,0,1,0,"" +,9,0,"editor",9,0,0,3,"page.TCEFORM.tt_content.header_position.disabled = 1 +page.TCEFORM.tt_content.colPos.readOnly = 1"
typo3/sysext/backend/Tests/Functional/Fixtures/pages.csv+8 −8 modified@@ -1,9 +1,9 @@ "pages" -,"uid","pid","sorting","title","deleted","perms_everybody" -,1,0,128,"Root",0,15 -,2,1,128,"Dummy 1-2",0,15 -,3,2,128,"Dummy 1-2-3",0,15 -,4,3,128,"Dummy 1-2-3-4",0,15 -,5,1,256,"Dummy 1-5",0,15 -,6,5,128,"Dummy 1-5-6",0,15 -,7,0,256,"Root 2",0,15 +,"uid","pid","sorting","title","deleted","perms_groupid","perms_group","perms_everybody" +,1,0,128,"Root",0,9,27,15 +,2,1,128,"Dummy 1-2",0,9,27,15 +,3,2,128,"Dummy 1-2-3",0,9,27,15 +,4,3,128,"Dummy 1-2-3-4",0,9,27,15 +,5,1,256,"Dummy 1-5",0,9,27,15 +,6,5,128,"Dummy 1-5-6",0,9,27,15 +,7,0,256,"Root 2",0,9,27,15
typo3/sysext/core/Classes/DataHandling/DataHandler.php+1 −1 modified@@ -214,7 +214,7 @@ class DataHandler implements LoggerAwareInterface * (thus UserTSconfig overrules externally set defaults which overrules TCA defaults) * * @var array - * @internal should only be used from within TYPO3 Core + * @internal should only be used from within DataHandler as permission checks do not apply to default values */ public $defaultValues = [];
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/TYPO3/typo3/commit/ac3f792bd5ab7c58153fc1075cb9e001c9cebe3bghsapatchWEB
- github.com/TYPO3/typo3/commit/cd11a19958d823d12d028f9345b41739c7e70118ghsapatchWEB
- github.com/TYPO3/typo3/commit/fb98378a8fd30dd50d89a3d1a420780819f38232ghsapatchWEB
- github.com/advisories/GHSA-5j7q-wmh7-cqhgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-59020ghsaADVISORY
- typo3.org/security/advisory/typo3-core-sa-2026-001ghsavendor-advisoryWEB
- github.com/TYPO3/typo3/security/advisories/GHSA-5j7q-wmh7-cqhgghsaWEB
News mentions
0No linked articles in our index yet.