‪TYPO3CMS  ‪main
SecureHtmlRenderingTest.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
18 use PHPUnit\Framework\Attributes\DataProvider;
19 use PHPUnit\Framework\Attributes\Test;
20 use Psr\Http\Message\ResponseInterface;
23 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
24 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
25 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\AbstractInstruction;
26 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\TypoScriptInstruction;
27 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
28 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
29 
30 final class ‪SecureHtmlRenderingTest extends FunctionalTestCase
31 {
33 
34  private const ‪TYPE_PLAIN = 'plain';
35  private const ‪TYPE_DISABLE_HTML_SANITIZE = 'disable-htmlSanitize';
36  protected const ‪LANGUAGE_PRESETS = [
37  'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8'],
38  ];
39 
40  protected array ‪$coreExtensionsToLoad = ['fluid_styled_content'];
41 
42  protected function ‪setUp(): void
43  {
44  parent::setUp();
45 
47  'acme-com',
48  $this->‪buildSiteConfiguration(1000, 'https://acme.com/'),
49  [$this->‪buildDefaultLanguageConfiguration('EN', 'https://acme.us/')]
50  );
51 
52  $this->withDatabaseSnapshot(function () {
53  $this->‪setUpDatabase();
54  });
55  }
56 
57  protected function ‪setUpDatabase(): void
58  {
59  $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
60  $backendUser = $this->setUpBackendUser(1);
61  ‪$GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser);
62 
63  $scenarioFile = __DIR__ . '/Fixtures/SecureHtmlScenario.yaml';
64  $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
65  $writer = DataHandlerWriter::withBackendUser($backendUser);
66  $writer->invokeFactory($factory);
67  static::failIfArrayIsNotEmpty(
68  $writer->getErrors()
69  );
70 
71  $this->setUpFrontendRootPage(
72  1000,
73  [
74  'constants' => ['EXT:fluid_styled_content/Configuration/TypoScript/constants.typoscript'],
75  'setup' => ['EXT:fluid_styled_content/Configuration/TypoScript/setup.typoscript'],
76  ],
77  [
78  'title' => 'ACME Root',
79  ]
80  );
81  }
82 
84  {
85  return [
86  '#01' => [
87  '01: <script>alert(1)</script>',
88  '<p>01: &lt;script&gt;alert(1)&lt;/script&gt;</p>',
89  ],
90  '#02' => [
91  '02: <unknown a="a" b="b">value</unknown>',
92  '<p>02: &lt;unknown a="a" b="b"&gt;value&lt;/unknown&gt;</p>',
93  ],
94  '#03' => [
95  '03: <img img="img" alt="alt" onerror="alert(1)">',
96  '<p>03: <img alt="alt"></p>',
97  ],
98  '#04' => [
99  '04: <img src="img" alt="alt" onerror="alert(1)">',
100  '<p>04: <img src="img" alt="alt"></p>',
101  ],
102  '#05' => [
103  '05: <img/src="img"/onerror="alert(1)">',
104  '<p>05: &lt;img/src="img"/onerror="alert(1)"&gt;</p>',
105  ],
106  '#06' => [
107  '06: <strong>Given that x < y and y > z...</strong>',
108  '<p>06: <strong>Given that x &lt; y and y &gt; z...</strong></p>',
109  ],
110  '#07' => [
111  '07: <a href="t3://page?uid=1000" target="_blank" rel="noreferrer" class="button" role="button" onmouseover="alert(1)">TYPO3</a>',
112  '<p>07: <a href="/" target="_blank" rel="noreferrer" class="button" role="button">TYPO3</a></p>',
113  ],
114  '#08' => [
115  '08: <?xml >s<img src=x onerror=alert(1)> ?>',
116  // Note: The TYPO3 HTML Parser encodes processing instructions, it's therefore
117  // expected and "OK" that the img tag is not encoded but sanitized.
118  // If the HTML Parser would not run, the expected result would be:
119  // '<p>08: &lt;?xml &gt;s&lt;img src=x onerror=alert(1)&gt; ?&gt;</p>',
120  '<p>08: &lt;?xml &gt;s<img src="x"> ?&gt;</p>',
121  ],
122  ];
123  }
124 
125  #[DataProvider('defaultParseFuncRteAvoidsCrossSiteScriptingDataProvider')]
126  #[Test]
127  public function ‪defaultParseFuncRteAvoidCrossSiteScripting(string $payload, string $expectation): void
128  {
129  $instructions = [
131  ];
132  $response = $this->‪invokeFrontendRendering(...$instructions);
133  self::assertSame($expectation, (string)$response->getBody());
134  }
135 
136  public static function ‪htmlViewHelperAvoidsCrossSiteScriptingDataProvider(): array
137  {
138  return [
139  '#01 ' . self::TYPE_PLAIN => [
141  '01: <script>alert(1)</script>',
142  '<p>01: &lt;script&gt;alert(1)&lt;/script&gt;</p>',
143  ],
144  '#01 ' . self::TYPE_DISABLE_HTML_SANITIZE => [
146  '01: <script>alert(1)</script>',
147  '<p>01: &lt;script&gt;alert(1)&lt;/script&gt;</p>',
148  ],
149  '#03 ' . self::TYPE_PLAIN => [
151  '03: <img img="img" alt="alt" onerror="alert(1)">',
152  '<p>03: <img alt="alt"></p>',
153  ],
154  '#03 ' . self::TYPE_DISABLE_HTML_SANITIZE => [
156  '03: <img img="img" alt="alt" onerror="alert(1)">',
157  '<p>03: <img img="img" alt="alt" onerror="alert(1)"></p>',
158  ],
159  '#07 ' . self::TYPE_PLAIN => [
161  '07: <a href="t3://page?uid=1000" target="_blank" rel="noreferrer" class="button" role="button" onmouseover="alert(1)">TYPO3</a>',
162  '<p>07: <a href="/" target="_blank" rel="noreferrer" class="button" role="button">TYPO3</a></p>',
163  ],
164  '#07 ' . self::TYPE_DISABLE_HTML_SANITIZE => [
166  '07: <a href="t3://page?uid=1000" target="_blank" rel="noreferrer" class="button" role="button" onmouseover="alert(1)">TYPO3</a>',
167  '<p>07: <a href="/" target="_blank" rel="noreferrer" class="button" role="button" onmouseover="alert(1)">TYPO3</a></p>',
168  ],
169  '#08 ' . self::TYPE_PLAIN => [
171  '08: <meta whatever="whatever">',
172  '<p>08: </p>',
173  ],
174  '#08 ' . self::TYPE_DISABLE_HTML_SANITIZE => [
176  '08: <meta whatever="whatever">',
177  '<p>08: <meta whatever="whatever"></p>',
178  ],
179  // `sdfield` is in `styles.content.allowTags` constant
180  '#09 ' . self::TYPE_PLAIN => [
182  '09: <sdfield onmouseover="alert(1)">',
183  '<p>09: &lt;sdfield onmouseover="alert(1)"&gt;&lt;/sdfield&gt;</p>',
184  ],
185  '#09 ' . self::TYPE_DISABLE_HTML_SANITIZE => [
187  '09: <sdfield onmouseover="alert(1)">',
188  '<p>09: <sdfield onmouseover="alert(1)"></p>',
189  ],
190  '#10 ' . self::TYPE_PLAIN => [
192  '10: <meta itemprop="type" content="voice">',
193  '<p>10: <meta itemprop="type" content="voice"></p>',
194  ],
195  '#10 ' . self::TYPE_DISABLE_HTML_SANITIZE => [
197  '10: <meta itemprop="type" content="voice">',
198  '<p>10: <meta itemprop="type" content="voice"></p>',
199  ],
200  ];
201  }
202 
203  #[DataProvider('htmlViewHelperAvoidsCrossSiteScriptingDataProvider')]
204  #[Test]
205  public function ‪htmlViewHelperAvoidsCrossSiteScripting(string $type, string $payload, string $expectation): void
206  {
207  $instructions = [
208  $this->‪createFluidTemplateContentObject($type, $payload),
209  ];
210  if ($type === self::TYPE_DISABLE_HTML_SANITIZE) {
211  $instructions[] = $this->‪createDisableHtmlSanitizeInstruction();
212  }
213  $response = $this->‪invokeFrontendRendering(...$instructions);
214  self::assertSame($expectation, trim((string)$response->getBody(), "\n"));
215  }
216 
217  public static function ‪customParseFuncAvoidsCrossSiteScriptingDataProvider(): array
218  {
219  return [
220  '#01' => [
221  '01: <script>alert(1)</script>',
222  '<p>01: &lt;script&gt;alert(1)&lt;/script&gt;</p>',
223  ],
224  '#02' => [
225  '02: <unknown a="a" b="b">value</unknown>',
226  '<p>02: &lt;unknown a="a" b="b"&gt;value&lt;/unknown&gt;</p>',
227  ],
228  '#03' => [
229  '03: <img img="img" alt="alt" onerror="alert(1)">',
230  '<p>03: <img alt="alt"></p>',
231  ],
232  '#04' => [
233  '04: <img src="img" alt="alt" onerror="alert(1)">',
234  '<p>04: <img src="img" alt="alt"></p>',
235  ],
236  '#05' => [
237  '05: <img/src="img"/onerror="alert(1)">',
238  '<p>05: <img src="img"></p>',
239  ],
240  '#06' => [
241  '06: <strong>Given that x < y and y > z...</strong>',
242  '<p>06: <strong>Given that x y and y &gt; z...</strong></p>',
243  ],
244  '#07' => [
245  '07: <a href="t3://page?uid=1000" target="_blank" rel="noreferrer" class="button" role="button" onmouseover="alert(1)">TYPO3</a>',
246  '<p>07: <a href="/" target="_blank" rel="noreferrer" class="button" role="button">TYPO3</a></p>',
247  ],
248  ];
249  }
250 
254  #[DataProvider('customParseFuncAvoidsCrossSiteScriptingDataProvider')]
255  #[Test]
256  public function ‪customParseFuncAvoidCrossSiteScripting(string $payload, string $expectation): void
257  {
258  $instructions = [
260  ];
261  $response = $this->‪invokeFrontendRendering(...$instructions);
262  self::assertSame($expectation, (string)$response->getBody());
263  }
264 
265  private function ‪invokeFrontendRendering(AbstractInstruction ...$instructions): ResponseInterface
266  {
267  $sourcePageId = 1100;
268 
269  $request = (new InternalRequest('https://acme.us/'))
270  ->withPageId($sourcePageId)
271  ->withInstructions(
272  [
274  ]
275  );
276 
277  if (count($instructions) > 0) {
278  $request = $this->‪applyInstructions($request, ...$instructions);
279  }
280 
281  return $this->executeFrontendSubRequest($request);
282  }
283 
284  private function ‪createDefaultInstruction(): TypoScriptInstruction
285  {
286  return (new TypoScriptInstruction())
287  ->withTypoScript([
288  'config.' => [
289  'no_cache' => 1,
290  'debug' => 0,
291  'admPanel' => 0,
292  'disableAllHeaderCode' => 1,
293  ],
294  'page' => 'PAGE',
295  'page.' => [
296  'typeNum' => 0,
297  ],
298  ]);
299  }
300 
301  private function ‪createTextContentObjectWithDefaultParseFuncRteInstruction(string $value): TypoScriptInstruction
302  {
303  // default configuration as shipped in ext:fluid_styled_content
304  return (new TypoScriptInstruction())
305  ->withTypoScript([
306  'page.' => [
307  '10' => 'TEXT',
308  '10.' => [
309  'value' => $value,
310  'parseFunc' => '< lib.parseFunc_RTE',
311  ],
312  ],
313  ]);
314  }
315 
316  private function ‪createTextContentObjectWithCustomParseFuncInstruction(string $value): TypoScriptInstruction
317  {
318  // basically considered "insecure setup"
319  // + no explicit htmlSanitize
320  // + no HTMLparser + HTMLparser.htmlSpecialChars
321  return (new TypoScriptInstruction())
322  ->withTypoScript([
323  'page.' => [
324  '10' => 'TEXT',
325  '10.' => [
326  'value' => $value,
327  'parseFunc.' => [
328  'allowTags' => 'a,img,sdfield',
329  'tags.' => [
330  'a' => 'TEXT',
331  'a.' => [
332  'current' => 1,
333  'typolink' => [
334  'parameter.' => [
335  'data' => 'parameters:href',
336  ],
337  'title.' => [
338  'data' => 'parameters:title',
339  ],
340  'ATagParams.' => [
341  'data' => 'parameters:allParams',
342  ],
343  ],
344  ],
345  ],
346  'nonTypoTagStdWrap.' => [
347  'encapsLines.' => [
348  'nonWrappedTag' => 'p',
349  ],
350  ],
351  ],
352  ],
353  ],
354  ]);
355  }
356 
357  private function ‪createDisableHtmlSanitizeInstruction(): TypoScriptInstruction
358  {
359  return (new TypoScriptInstruction())
360  ->withTypoScript([
361  'lib.' => [
362  'parseFunc_RTE.' => [
363  'htmlSanitize' => '0',
364  ],
365  ],
366  ]);
367  }
368 
369  private function ‪createFluidTemplateContentObject(string $type, string $payload): TypoScriptInstruction
370  {
371  return (new TypoScriptInstruction())
372  ->withTypoScript([
373  'page.' => [
374  '10' => 'FLUIDTEMPLATE',
375  '10.' => [
376  'file' => 'EXT:fluid_styled_content/Tests/Functional/Rendering/Fixtures/FluidTemplate.html',
377  'variables.' => [
378  'type' => 'TEXT',
379  'type.' => [
380  'value' => $type,
381  ],
382  'payload' => 'TEXT',
383  'payload.' => [
384  'value' => $payload,
385  ],
386  ],
387  ],
388  ],
389  ]);
390  }
391 }
‪TYPO3\CMS\Core\Localization\LanguageServiceFactory
Definition: LanguageServiceFactory.php:25
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\invokeFrontendRendering
‪invokeFrontendRendering(AbstractInstruction ... $instructions)
Definition: SecureHtmlRenderingTest.php:264
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\customParseFuncAvoidsCrossSiteScriptingDataProvider
‪static customParseFuncAvoidsCrossSiteScriptingDataProvider()
Definition: SecureHtmlRenderingTest.php:216
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait
Definition: SiteBasedTestTrait.php:37
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest
Definition: SecureHtmlRenderingTest.php:31
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\writeSiteConfiguration
‪writeSiteConfiguration(string $identifier, array $site=[], array $languages=[], array $errorHandling=[])
Definition: SiteBasedTestTrait.php:50
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\createDisableHtmlSanitizeInstruction
‪createDisableHtmlSanitizeInstruction()
Definition: SecureHtmlRenderingTest.php:356
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\buildSiteConfiguration
‪buildSiteConfiguration(int $rootPageId, string $base='')
Definition: SiteBasedTestTrait.php:88
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\defaultParseFuncRteAvoidCrossSiteScripting
‪defaultParseFuncRteAvoidCrossSiteScripting(string $payload, string $expectation)
Definition: SecureHtmlRenderingTest.php:126
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\applyInstructions
‪applyInstructions(InternalRequest $request, AbstractInstruction ... $instructions)
Definition: SiteBasedTestTrait.php:205
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\htmlViewHelperAvoidsCrossSiteScripting
‪htmlViewHelperAvoidsCrossSiteScripting(string $type, string $payload, string $expectation)
Definition: SecureHtmlRenderingTest.php:204
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\TYPE_DISABLE_HTML_SANITIZE
‪const TYPE_DISABLE_HTML_SANITIZE
Definition: SecureHtmlRenderingTest.php:34
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\setUpDatabase
‪setUpDatabase()
Definition: SecureHtmlRenderingTest.php:56
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\setUp
‪setUp()
Definition: SecureHtmlRenderingTest.php:41
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\createTextContentObjectWithDefaultParseFuncRteInstruction
‪createTextContentObjectWithDefaultParseFuncRteInstruction(string $value)
Definition: SecureHtmlRenderingTest.php:300
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\createDefaultInstruction
‪createDefaultInstruction()
Definition: SecureHtmlRenderingTest.php:283
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\htmlViewHelperAvoidsCrossSiteScriptingDataProvider
‪static htmlViewHelperAvoidsCrossSiteScriptingDataProvider()
Definition: SecureHtmlRenderingTest.php:135
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\TYPE_PLAIN
‪const TYPE_PLAIN
Definition: SecureHtmlRenderingTest.php:33
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\$coreExtensionsToLoad
‪array $coreExtensionsToLoad
Definition: SecureHtmlRenderingTest.php:39
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\buildDefaultLanguageConfiguration
‪buildDefaultLanguageConfiguration(string $identifier, string $base)
Definition: SiteBasedTestTrait.php:98
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\LANGUAGE_PRESETS
‪const LANGUAGE_PRESETS
Definition: SecureHtmlRenderingTest.php:35
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\customParseFuncAvoidCrossSiteScripting
‪customParseFuncAvoidCrossSiteScripting(string $payload, string $expectation)
Definition: SecureHtmlRenderingTest.php:255
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering
Definition: SecureHtmlRenderingTest.php:16
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\createFluidTemplateContentObject
‪createFluidTemplateContentObject(string $type, string $payload)
Definition: SecureHtmlRenderingTest.php:368
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\createTextContentObjectWithCustomParseFuncInstruction
‪createTextContentObjectWithCustomParseFuncInstruction(string $value)
Definition: SecureHtmlRenderingTest.php:315
‪TYPO3\CMS\FluidStyledContent\Tests\Functional\Rendering\SecureHtmlRenderingTest\defaultParseFuncRteAvoidsCrossSiteScriptingDataProvider
‪static defaultParseFuncRteAvoidsCrossSiteScriptingDataProvider()
Definition: SecureHtmlRenderingTest.php:82