TYPO3 contains Sensitive Information Disclosure via YAML Placeholder Expressions in Site Configuration
Description
TYPO3 is an open source PHP based web content management system. Versions prior to 9.5.38, 10.4.33, 11.5.20, and 12.1.1 are subject to Sensitive Information Disclosure. Due to the lack of handling user-submitted YAML placeholder expressions in the site configuration backend module, attackers could expose sensitive internal information, such as system configuration or HTTP request messages of other website visitors. A valid backend user account having administrator privileges is needed to exploit this vulnerability. This issue has been patched in versions 9.5.38 ELTS, 10.4.33, 11.5.20, 12.1.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
TYPO3 CMS site configuration module does not sanitize YAML placeholder expressions, allowing admin attackers to disclose sensitive system and user data.
Root cause
CVE-2022-23504 is a sensitive information disclosure vulnerability in the TYPO3 CMS, an open-source PHP-based web content management system. The flaw exists in the site configuration backend module, which fails to properly handle user-submitted YAML placeholder expressions [1][3]. This means that an authenticated administrator with backend access can inject these expressions into the configuration.
Attack vector
To exploit this vulnerability, an attacker must have a valid backend user account with administrator privileges [1]. The attacker then crafts YAML placeholder expressions within the site configuration interface, which the system processes without proper sanitization [3]. The attack surface is limited to the backend module used for site configuration, requiring prior authenticated access.
Impact
Successful exploitation allows the attacker to expose sensitive internal information, such as the full system configuration and the HTTP request messages of other website visitors [1]. This level of information disclosure can compromise the confidentiality of both the server environment and the data of other users.
Mitigation
The vulnerability has been patched in TYPO3 versions 9.5.38 ELTS, 10.4.33, 11.5.20, and 12.1.1 [1]. Administrators should upgrade to these or later versions. The patch introduces a YamlPlaceholderGuard class that prevents the introduction of new YAML placeholders through the user interface [3].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
typo3/cms-corePackagist | >= 9.0.0, < 9.5.38 | 9.5.38 |
typo3/cms-corePackagist | >= 10.0.0, < 10.4.33 | 10.4.33 |
typo3/cms-corePackagist | >= 11.0.0, < 11.5.20 | 11.5.20 |
typo3/cms-corePackagist | >= 12.0.0, < 12.1.1 | 12.1.1 |
typo3/cmsPackagist | >= 10.0.0, < 10.4.33 | 10.4.33 |
typo3/cmsPackagist | >= 11.0.0, < 11.5.20 | 11.5.20 |
typo3/cmsPackagist | >= 12.0.0, < 12.1.1 | 12.1.1 |
Affected products
4- osv-coords3 versions
>= 9.0.0, < 9.5.38+ 2 more
- (no CPE)range: >= 9.0.0, < 9.5.38
- (no CPE)range: >= 10.0.0, < 10.4.33
- (no CPE)range: >= 9.0.0, < 9.5.38
Patches
1d1e627ff7eef[SECURITY] Disallow introducing Yaml placeholders in user interface
8 files changed · +208 −5
typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php+1 −1 modified@@ -411,7 +411,7 @@ protected function saveAction(ServerRequestInterface $request): ResponseInterfac $siteConfigurationManager->rename($currentIdentifier, $siteIdentifier); $this->getBackendUser()->writelog(Type::SITE, SiteAction::RENAME, SystemLogErrorClassification::MESSAGE, 0, 'Site configuration \'%s\' was renamed to \'%s\'.', [$currentIdentifier, $siteIdentifier], 'site'); } - $siteConfigurationManager->write($siteIdentifier, $newSiteConfiguration); + $siteConfigurationManager->write($siteIdentifier, $newSiteConfiguration, true); if ($isNewConfiguration) { $this->getBackendUser()->writelog(Type::SITE, SiteAction::CREATE, SystemLogErrorClassification::MESSAGE, 0, 'Site configuration \'%s\' was created.', [$siteIdentifier], 'site'); } else {
typo3/sysext/core/Classes/Configuration/Loader/Exception/YamlPlaceholderException.php+22 −0 added@@ -0,0 +1,22 @@ +<?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\Core\Configuration\Loader\Exception; + +class YamlPlaceholderException extends \RuntimeException +{ +}
typo3/sysext/core/Classes/Configuration/Loader/YamlFileLoader.php+2 −1 modified@@ -47,6 +47,7 @@ class YamlFileLoader implements LoggerAwareInterface { use LoggerAwareTrait; + public const PATTERN_PARTS = '%[^(%]+?\([\'"]?([^(]*?)[\'"]?\)%|%([^%()]*?)%'; public const PROCESS_PLACEHOLDERS = 1; public const PROCESS_IMPORTS = 2; @@ -252,7 +253,7 @@ protected function getParts(string $placeholders): array // find occurrences of placeholders like %some()% and %array.access%. // Only find the innermost ones, so we can nest them. preg_match_all( - '/%[^(%]+?\([\'"]?([^(]*?)[\'"]?\)%|%([^%()]*?)%/', + '/' . self::PATTERN_PARTS . '/', $placeholders, $parts, PREG_UNMATCHED_AS_NULL
typo3/sysext/core/Classes/Configuration/Loader/YamlPlaceholderGuard.php+102 −0 added@@ -0,0 +1,102 @@ +<?php + +/* + * 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\Core\Configuration\Loader; + +use TYPO3\CMS\Core\Configuration\Loader\Exception\YamlPlaceholderException; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\String\StringFragmentPattern; +use TYPO3\CMS\Core\Utility\String\StringFragmentSplitter; + +/** + * A guard for protecting YAML placeholders - keeps existing, but escalates on adding new placeholders + */ +class YamlPlaceholderGuard +{ + protected StringFragmentSplitter $fragmentSplitter; + + public function __construct(protected array $existingConfiguration) + { + $fragmentPattern = GeneralUtility::makeInstance( + StringFragmentPattern::class, + StringFragmentSplitter::TYPE_EXPRESSION, + YamlFileLoader::PATTERN_PARTS + ); + $this->fragmentSplitter = GeneralUtility::makeInstance( + StringFragmentSplitter::class, + $fragmentPattern + ); + } + + /** + * Modifies existing configuration. + */ + public function process(array $modified): array + { + return $this->protectPlaceholders($this->existingConfiguration, $modified); + } + + /** + * Detects placeholders that have been introduced and handles* them. + * (*) currently throws an exception, but could be purged or escaped as well + * + * @param array<string, mixed> $current + * @param array<string, mixed> $modified + * @param list<string> $steps configuration keys traversed so far + * @return array<string, mixed> sanitized configuration (currently not used, exception thrown before) + * @throws YamlPlaceholderException + */ + protected function protectPlaceholders(array $current, array $modified, array $steps = []): array + { + foreach ($modified as $key => $value) { + $currentSteps = array_merge($steps, [$key]); + if (is_array($value)) { + $modified[$key] = $this->protectPlaceholders( + $current[$key] ?? [], + $value, + $currentSteps + ); + } elseif (is_string($value)) { + $splitFlags = StringFragmentSplitter::FLAG_UNMATCHED_AS_NULL; + $newFragments = $this->fragmentSplitter->split($value, $splitFlags); + if (is_string($current[$key] ?? null)) { + $currentFragments = $this->fragmentSplitter->split($current[$key] ?? '', $splitFlags); + } else { + $currentFragments = null; + } + // in case there are new fragments (at least one matching the pattern) + if ($newFragments !== null) { + // compares differences in `expression` fragments only + $differences = $currentFragments === null + ? $newFragments->withOnlyType(StringFragmentSplitter::TYPE_EXPRESSION) + : $newFragments->withOnlyType(StringFragmentSplitter::TYPE_EXPRESSION) + ->diff($currentFragments->withOnlyType(StringFragmentSplitter::TYPE_EXPRESSION)); + if (count($differences) > 0) { + throw new YamlPlaceholderException( + sprintf( + 'Introducing placeholder%s %s for %s is not allowed', + count($differences) !== 1 ? 's' : '', + implode(', ', $differences->getFragments()), + implode('.', $currentSteps) + ), + 1651690534 + ); + } + } + } + } + return $modified; + } +}
typo3/sysext/core/Classes/Configuration/SiteConfiguration.php+30 −1 modified@@ -26,7 +26,9 @@ use TYPO3\CMS\Core\Configuration\Event\SiteConfigurationBeforeWriteEvent; use TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent; use TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException; +use TYPO3\CMS\Core\Configuration\Loader\Exception\YamlPlaceholderException; use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader; +use TYPO3\CMS\Core\Configuration\Loader\YamlPlaceholderGuard; use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Site\Entity\Site; @@ -257,15 +259,20 @@ public function writeSettings(string $siteIdentifier, array $settings): void /** * Add or update a site configuration * + * @param bool $protectPlaceholders whether to disallow introducing new placeholders + * @todo enforce $protectPlaceholders with TYPO3 v13.0 * @throws SiteConfigurationWriteException */ - public function write(string $siteIdentifier, array $configuration): void + public function write(string $siteIdentifier, array $configuration, bool $protectPlaceholders = false): void { $folder = $this->configPath . '/' . $siteIdentifier; $fileName = $folder . '/' . $this->configFileName; $newConfiguration = $configuration; if (!file_exists($folder)) { GeneralUtility::mkdir_deep($folder); + if ($protectPlaceholders && $newConfiguration !== []) { + $newConfiguration = $this->protectPlaceholders([], $newConfiguration); + } } elseif (file_exists($fileName)) { $loader = GeneralUtility::makeInstance(YamlFileLoader::class); // load without any processing to have the unprocessed base to modify @@ -277,6 +284,9 @@ public function write(string $siteIdentifier, array $configuration): void self::findRemoved($processed, $configuration), self::findModified($processed, $configuration) ); + if ($protectPlaceholders && $newModified !== []) { + $newModified = $this->protectPlaceholders($newConfiguration, $newModified); + } // change _only_ the modified keys, leave the original non-changed areas alone ArrayUtility::mergeRecursiveWithOverrule($newConfiguration, $newModified); } @@ -327,6 +337,25 @@ public function delete(string $siteIdentifier): void $this->firstLevelCache = null; } + /** + * Detects placeholders that have been introduced and handles* them. + * (*) currently throws an exception, but could be purged or escaped as well + * + * @param array<string, mixed> $existingConfiguration + * @param array<string, mixed> $modifiedConfiguration + * @return array<string, mixed> sanitized configuration (currently not used, exception thrown before) + * @throws SiteConfigurationWriteException + */ + protected function protectPlaceholders(array $existingConfiguration, array $modifiedConfiguration): array + { + try { + return GeneralUtility::makeInstance(YamlPlaceholderGuard::class, $existingConfiguration) + ->process($modifiedConfiguration); + } catch (YamlPlaceholderException $exception) { + throw new SiteConfigurationWriteException($exception->getMessage(), 1670361271, $exception); + } + } + protected function sortConfiguration(array $newConfiguration): array { ksort($newConfiguration);
typo3/sysext/core/Tests/Unit/Configuration/Fixtures/SiteConfigs/config2_expected.yaml+1 −0 modified@@ -3,6 +3,7 @@ baseVariants: - base: foo123 condition: bar +customProperty: 'Using %env("existing")% variable' errorHandling: - errorCode: '404'
typo3/sysext/core/Tests/Unit/Configuration/Fixtures/SiteConfigs/config2.yaml+1 −0 modified@@ -3,6 +3,7 @@ baseVariants: - base: foo123 condition: bar +customProperty: 'Using %env("existing")% variable' errorHandling: - errorCode: '404'
typo3/sysext/core/Tests/Unit/Configuration/SiteConfigurationTest.php+49 −2 modified@@ -19,6 +19,7 @@ use Symfony\Component\Yaml\Yaml; use TYPO3\CMS\Core\Cache\Frontend\NullFrontend; +use TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException; use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader; use TYPO3\CMS\Core\Configuration\SiteConfiguration; use TYPO3\CMS\Core\Core\Environment; @@ -107,7 +108,7 @@ public function writeOnlyWritesModifiedKeys(): void // delete values unset($configuration['someOtherValue'], $configuration['languages'][1]); - $this->siteConfiguration->write($identifier, $configuration); + $this->siteConfiguration->write($identifier, $configuration, true); // expect modified base but intact imports self::assertFileEquals($expected, $siteConfig); @@ -146,9 +147,55 @@ public function writingOfNestedStructuresPreservesOrder(): void 'navigationTitle' => 'English', ]; array_unshift($configuration['languages'], $languageConfig); - $this->siteConfiguration->write($identifier, $configuration); + $this->siteConfiguration->write($identifier, $configuration, true); // expect modified base but intact imports self::assertFileEquals($expected, $siteConfig); } + + public static function writingPlaceholdersIsHandledDataProvider(): \Generator + { + yield 'unchanged' => [ + ['customProperty' => 'Using %env("existing")% variable'], + false, + ]; + yield 'removed placeholder variable' => [ + ['customProperty' => 'Not using any variable'], + false, + ]; + yield 'changed raw text only' => [ + ['customProperty' => 'Using %env("existing")% variable from system environment'], + false, + ]; + yield 'added new placeholder variable' => [ + ['customProperty' => 'Using %env("existing")% and %env("secret")% variable'], + true, + ]; + } + + /** + * @test + * @dataProvider writingPlaceholdersIsHandledDataProvider + */ + public function writingPlaceholdersIsHandled(array $changes, bool $expectedException): void + { + if ($expectedException) { + $this->expectException(SiteConfigurationWriteException::class); + $this->expectExceptionCode(1670361271); + } + + $identifier = 'testsite'; + GeneralUtility::mkdir_deep($this->fixturePath . '/' . $identifier); + $configFixture = __DIR__ . '/Fixtures/SiteConfigs/config2.yaml'; + $siteConfig = $this->fixturePath . '/' . $identifier . '/config.yaml'; + copy($configFixture, $siteConfig); + // load with resolved imports as the module does + $configuration = GeneralUtility::makeInstance(YamlFileLoader::class) + ->load( + GeneralUtility::fixWindowsFilePath($siteConfig), + YamlFileLoader::PROCESS_IMPORTS + ); + $configuration = array_merge($configuration, $changes); + $this->siteConfiguration->write($identifier, $configuration, true); + } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-8w3p-qh3x-6gjrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-23504ghsaADVISORY
- github.com/FriendsOfPHP/security-advisories/blob/master/typo3/cms-core/CVE-2022-23504.yamlghsaWEB
- github.com/FriendsOfPHP/security-advisories/blob/master/typo3/cms/CVE-2022-23504.yamlghsaWEB
- github.com/TYPO3/typo3/commit/d1e627ff7eef07bd94c53db861e85977b203900aghsaWEB
- github.com/TYPO3/typo3/security/advisories/GHSA-8w3p-qh3x-6gjrghsax_refsource_CONFIRMWEB
- typo3.org/security/advisory/typo3-core-sa-2022-016ghsaWEB
News mentions
0No linked articles in our index yet.