VYPR
Moderate severityNVD Advisory· Published Dec 14, 2022· Updated Apr 21, 2025

TYPO3 contains Sensitive Information Disclosure via YAML Placeholder Expressions in Site Configuration

CVE-2022-23504

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.

PackageAffected versionsPatched versions
typo3/cms-corePackagist
>= 9.0.0, < 9.5.389.5.38
typo3/cms-corePackagist
>= 10.0.0, < 10.4.3310.4.33
typo3/cms-corePackagist
>= 11.0.0, < 11.5.2011.5.20
typo3/cms-corePackagist
>= 12.0.0, < 12.1.112.1.1
typo3/cmsPackagist
>= 10.0.0, < 10.4.3310.4.33
typo3/cmsPackagist
>= 11.0.0, < 11.5.2011.5.20
typo3/cmsPackagist
>= 12.0.0, < 12.1.112.1.1

Affected products

4

Patches

1
d1e627ff7eef

[SECURITY] Disallow introducing Yaml placeholders in user interface

https://github.com/TYPO3/typo3Oliver HaderDec 13, 2022via ghsa
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

News mentions

0

No linked articles in our index yet.