CVE-2024-28191: Insufficient Information (NVD-CWE-noinfo)
value: Contao is an open source content management system. Starting in version 4.0.0 and prior to version 4.13.40 and 5.3.4, it is possible to inject insert tags in frontend forms if the output is structured in a very specific way. Contao versions 4.13.40 and 5.3.4 have a patch for this issue. As a workaround, do not output user data from frontend forms next to each other, always separate them by at least one character.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-09T14:15Z
Repository: https://github.com/contao/contao
Language: PHP
Files changed: core-bundle/contao/library/Contao/Input.php, core-bundle/src/String/SimpleTokenParser.php, core-bundle/tests/Contao/InputTest.php, core-bundle/tests/String/SimpleTokenParserTest.php
Commit date: 2024-04-09 07:19:09 +0200
Commit message: Merge pull request from GHSA-747v-52c4-8vj8

Co-authored-by: Leo Feyer <1192057+leofeyer@users.noreply.github.com>
Diff: diff --git a/core-bundle/contao/library/Contao/Input.php b/core-bundle/contao/library/Contao/Input.php
index 896c22fedb9..0395efdaa8c 100644
--- a/core-bundle/contao/library/Contao/Input.php
+++ b/core-bundle/contao/library/Contao/Input.php
@@ -1057,11 +1057,14 @@ class Input
 			}
 
 			return $varValue;
 		}
 
-		return str_replace(array('{{', '}}'), array('&#123;&#123;', '&#125;&#125;'), (string) $varValue);
+		$varValue = str_replace(array('{{', '}}'), array('&#123;&#123;', '&#125;&#125;'), (string) $varValue);
+
+		// Encode single curly braces at the beginning and end of the string
+		return preg_replace(array('/^(\s*)\{|\{(\s*)$/', '/^(\s*)\}|\}(\s*)$/'), array('$1&#123;$2', '$1&#125;$2'), $varValue);
 	}
 
 	/**
 	 * Fallback to the session form data if there is no post data
 	 *
diff --git a/core-bundle/src/String/SimpleTokenParser.php b/core-bundle/src/String/SimpleTokenParser.php
index 217cded2727..eca699b0a9c 100644
--- a/core-bundle/src/String/SimpleTokenParser.php
+++ b/core-bundle/src/String/SimpleTokenParser.php
@@ -10,10 +10,11 @@ declare(strict_types=1);
  * @license LGPL-3.0-or-later
  */
 
 namespace Contao\CoreBundle\String;
 
+use Contao\Input;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
 use Psr\Log\LogLevel;
 use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
 use Symfony\Component\ExpressionLanguage\Lexer;
@@ -106,11 +107,11 @@ class SimpleTokenParser implements LoggerAwareInterface
                     $this->logger?->log(LogLevel::INFO, sprintf('Tried to parse unknown simple token "%s".', $matches[1]));
 
                     return '##'.$matches[1].'##';
                 }
 
-                return $data[$matches[1]];
+                return Input::encodeInsertTags($data[$matches[1]]);
             },
             $subject,
         );
     }
 
diff --git a/core-bundle/tests/Contao/InputTest.php b/core-bundle/tests/Contao/InputTest.php
index 92ade60d33b..48ece4b039b 100644
--- a/core-bundle/tests/Contao/InputTest.php
+++ b/core-bundle/tests/Contao/InputTest.php
@@ -166,12 +166,12 @@ class InputTest extends TestCase
     /**
      * @group legacy
      */
     public function testEncodesInsertTags(): void
     {
-        $source = '{{ foo }}';
-        $encoded = '&#123;&#123; foo &#125;&#125;';
+        $source = ' {{ foo }} { bar } ';
+        $encoded = ' &#123;&#123; foo &#125;&#125; { bar &#125; ';
 
         $_GET = $_POST = $_COOKIE = [
             'key' => $source,
             $source => 'value',
         ];
@@ -325,18 +325,18 @@ class InputTest extends TestCase
     /**
      * @dataProvider encodeNoneModeProvider
      *
      * @group legacy
      */
-    public function testEncodeNoneMode(string $source, string $expected, string|null $expectedEncoded = null): void
+    public function testEncodeNoneMode(string $source, string $expected, string|null $expectedEncoded = null, string|null $expectedEncodedDouble = null): void
     {
         $expectedEncoded ??= $expected;
 
         $this->assertSame($expected, Input::encodeInput($source, InputEncodingMode::encodeNone, false));
         $this->assertSame($expectedEncoded, Input::encodeInput($source, InputEncodingMode::encodeNone));
         $this->assertSame($expected.$expected, Input::encodeInput($source.$source, InputEncodingMode::encodeNone, false));
-        $this->assertSame($expectedEncoded.$expectedEncoded, Input::encodeInput($source.$source, InputEncodingMode::encodeNone));
+        $this->assertSame($expectedEncodedDouble ?? $expectedEncoded.$expectedEncoded, Input::encodeInput($source.$source, InputEncodingMode::encodeNone));
 
         System::getContainer()->set('request_stack', $stack = new RequestStack());
         $stack->push(new Request([], ['key' => $source]));
 
         $this->assertSame($expectedEncoded, Input::postRaw('key'));
@@ -353,14 +353,16 @@ class InputTest extends TestCase
     {
         yield ['', ''];
         yield ['foo', 'foo'];
         yield ['\X \0 \X', '\X &#92;0 \X'];
         yield ["a\rb\r\nc\n\rd\ne", "a\nb\nc\n\nd\ne"];
-        yield ['{}', '{}'];
+        yield ['{}', '{}', '&#123;&#125;', '&#123;}{&#125;'];
         yield ['{{}}', '{{}}', '&#123;&#123;&#125;&#125;'];
-        yield ['{{{}}}', '{{{}}}', '&#123;&#123;{&#125;&#125;}'];
+        yield ['{{{}}}', '{{{}}}', '&#123;&#123;{&#125;&#125;&#125;', '&#123;&#123;{&#125;&#125;}&#123;&#123;{&#125;&#125;&#125;'];
         yield ['{{{{}}}}', '{{{{}}}}', '&#123;&#123;&#123;&#123;&#125;&#125;&#125;&#125;'];
+        yield ['{ start {and} end }', '{ start {and} end }', '&#123; start {and} end &#125;', '&#123; start {and} end }{ start {and} end &#125;'];
+        yield ["\n\t { foo }\n\t ", "\n\t { foo }\n\t ", "\n\t &#123; foo &#125;\n\t ", "\n\t &#123; foo }\n\t \n\t { foo &#125;\n\t "];
         yield ["\0", "\u{FFFD}"];
         yield ["\x80", "\u{FFFD}"];
         yield ["\xFF", "\u{FFFD}"];
         yield ["\xC2\x7F", "\u{FFFD}\x7F"];
         yield ["\xC2\x80", "\xC2\x80"];
diff --git a/core-bundle/tests/String/SimpleTokenParserTest.php b/core-bundle/tests/String/SimpleTokenParserTest.php
index 5ab5aaa7671..5aa48136eac 100644
--- a/core-bundle/tests/String/SimpleTokenParserTest.php
+++ b/core-bundle/tests/String/SimpleTokenParserTest.php
@@ -143,20 +143,38 @@ class SimpleTokenParserTest extends TestCase
             'This is my {if value!==5.0}match{endif}',
             ['value' => 5.0],
             'This is my ',
         ];
 
+        yield 'Test regular curly braces do not get encoded' => [
+            '##token##',
+            ['token' => 'foo { bar } baz'],
+            'foo { bar } baz',
+        ];
+
         yield 'Test if-tags insertion not evaluated' => [
             '##token##',
             ['token' => '{if token=="foo"}'],
-            '{if token=="foo"}',
+            '&#123;if token=="foo"&#125;',
+        ];
+
+        yield 'Test insert tags insertion not possible' => [
+            '##token##',
+            ['token' => '{{date}}'],
+            '&#123;&#123;date&#125;&#125;',
         ];
 
         yield 'Test if-tags insertion not evaluated with multiple tokens' => [
             '##token1####token2####token3##',
             ['token1' => '{', 'token2' => 'if', 'token3' => ' token=="foo"}'],
-            '{if token=="foo"}',
+            '&#123;if token=="foo"&#125;',
+        ];
+
+        yield 'Test insert tags insertion not possible with multiple tokens' => [
+            '##token1####token2####token3##',
+            ['token1' => '{', 'token2' => '{date}', 'token3' => '}'],
+            '&#123;&#123;date&#125;&#125;',
         ];
 
         yield 'Test escaping works correctly' => [
             '{if value=="f\"oo"}match{endif}',
             ['value' => 'f"oo'],

Vulnerable code: 		return str_replace(array('{{', '}}'), array('&#123;&#123;', '&#125;&#125;'), (string) $varValue);
                return $data[$matches[1]];
        $source = '{{ foo }}';
        $encoded = '&#123;&#123; foo &#125;&#125;';
    public function testEncodeNoneMode(string $source, string $expected, string|null $expectedEncoded = null): void
        $this->assertSame($expectedEncoded.$expectedEncoded, Input::encodeInput($source.$source, InputEncodingMode::encodeNone));
        yield ['{}', '{}'];
        yield ['{{{}}}', '{{{}}}', '&#123;&#123;{&#125;&#125;}'];
            '{if token=="foo"}',
            '{if token=="foo"}',
Fixed code: 		$varValue = str_replace(array('{{', '}}'), array('&#123;&#123;', '&#125;&#125;'), (string) $varValue);

		// Encode single curly braces at the beginning and end of the string
		return preg_replace(array('/^(\s*)\{|\{(\s*)$/', '/^(\s*)\}|\}(\s*)$/'), array('$1&#123;$2', '$1&#125;$2'), $varValue);
use Contao\Input;
                return Input::encodeInsertTags($data[$matches[1]]);
        $source = ' {{ foo }} { bar } ';
        $encoded = ' &#123;&#123; foo &#125;&#125; { bar &#125; ';
    public function testEncodeNoneMode(string $source, string $expected, string|null $expectedEncoded = null, string|null $expectedEncodedDouble = null): void
        $this->assertSame($expectedEncodedDouble ?? $expectedEncoded.$expectedEncoded, Input::encodeInput($source.$source, InputEncodingMode::encodeNone));
        yield ['{}', '{}', '&#123;&#125;', '&#123;}{&#125;'];
        yield ['{{{}}}', '{{{}}}', '&#123;&#123;{&#125;&#125;&#125;', '&#123;&#123;{&#125;&#125;}&#123;&#123;{&#125;&#125;&#125;'];
        yield ['{ start {and} end }', '{ start {and} end }', '&#123; start {and} end &#125;', '&#123; start {and} end }{ start {and} end &#125;'];
        yield ["\n\t { foo }\n\t ", "\n\t { foo }\n\t ", "\n\t &#123; foo &#125;\n\t ", "\n\t &#123; foo }\n\t \n\t { foo &#125;\n\t "];
        yield 'Test regular curly braces do not get encoded' => [
            '##token##',
            ['token' => 'foo { bar } baz'],
            'foo { bar } baz',
        ];

            '&#123;if token=="foo"&#125;',
        ];

        yield 'Test insert tags insertion not possible' => [
            '##token##',
            ['token' => '{{date}}'],
            '&#123;&#123;date&#125;&#125;',
            '&#123;if token=="foo"&#125;',
        ];

        yield 'Test insert tags insertion not possible with multiple tokens' => [
            '##token1####token2####token3##',
            ['token1' => '{', 'token2' => '{date}', 'token3' => '}'],
            '&#123;&#123;date&#125;&#125;',