HTMLSanitizer in TYPO3

How to Troubleshoot Issues with the New Sanitizer.

all included

Alexander Böhm
Unser Spezialist für TYPO3 Backend-Berechtigungen, TypoScript und Integrations-Themen.
Reading duration: approx. 3 Minutes

The TYPO3 security update released on August 10, 2021, introduced an HTML sanitizer to TYPO3 (https://typo3.org/article/typo3-core-sa-2021-013). The affected versions are 7 ELTS, 8 ELTS, 9, 10, and 11.

The security issue is that a user with " Backend" privileges could insert malicious HTML code via the RTE, and the existing mechanisms in TYPO3 are not sufficient to completely remove malicious code.

The HTML sanitizer’s task is now to cleanse the HTML output of malicious content and thus close potential cross-site scripting vulnerabilities.

However, during our updates to TYPO3 8, 9, and 10, HTML attributes were suddenly removed and HTML tags were escaped in various places. These include, for example, our forms created with the Form Framework, as well as the 'figure' tag for images or the 'loading' attribute in the 'img' tag for lazy loading. In this article, we’ll therefore show you how to extend or disable the sanitizer accordingly to restore the website’s functionality.

Disabling the Sanitizer Globally—Possible, but Not a Good Solution

If the sanitizer is causing serious problems, you have the option to disable it globally.
To do this, you only need the following two lines in TypoScript:

lib.parseFunc.htmlSanitize = 0
lib.parseFunc_RTE.htmlSanitize = 0

However, the sanitizer is now part of the security update. Therefore, globally disabling the sanitizer should only be considered in exceptional cases and should not be a permanent measure.

Disabling the Sanitizer on a Case-by-Case Basis—Much Better

A better solution is to disable the Sanitizer only in specific places. Here’s an example from the TYPO3 documentation:

// or disable individually per use-case
10 = TEXT
10 {
  value = <div><img src="invalid.file" onerror="alert(1)"></div>
  parseFunc =< lib.parseFunc_RTE
  parseFunc.htmlSanitize = 0
}

This allows you to intentionally exclude certain areas of the page from the sanitizer, while still maintaining protection for the other areas. The excluded section should then contain only controllable content that does not originate from outside the page, such as user input via the front end.

Note: The sanitizer is triggered by calling stdWrap.parseFunc.

This can be done explicitly by you—for example, in a Fluid template—or implicitly, such as by using the Fluid view helper 'f:format.html'. Incidentally, converting to 'f:format.raw' is usually not a good solution here, since, among other things, links are no longer converted to the correct format.

Extending the Sanitizer—Registering Custom Tags and Attributes

Another alternative is to extend the sanitizer with additional tags and attributes.

Warning: You are extending an internal API and should carefully consider which attributes and tags you allow!

For this, you’ll need a PHP class, which should be stored in an extension, such as a site extension.
In our case, the class is located at /Classes/Sanitizer/CustomHtmlSanitizer.php:

<?php
 
declare(strict_types=1);
 
namespace PunktDe\PtSite\Sanitizer;
 
use TYPO3\CMS\Core\Html\DefaultSanitizerBuilder;
use TYPO3\HtmlSanitizer\Behavior;
use TYPO3\HtmlSanitizer\Behavior\Attr;
use TYPO3\HtmlSanitizer\Behavior\Tag;
 
class CustomHtmlSanitizer extends DefaultSanitizerBuilder
{
    public function createBehavior(): Behavior
    {
        // ...
    }
}

The 'createBehavior()' method now allows us to add additional tags and attributes to the sanitizer.

In the following example, we add the 'figure' tag to the sanitizer and allow all general (global) HTML attributes for this tag. These are already defined in the sanitizer package (\TYPO3\HtmlSanitizer\Builder\CommonBuilder).

public function createBehavior(): Behavior
    {
        // extends existing behavior, adds new tag
        return parent::createBehavior()
            ->withName('common')
            ->withTags(
                (new Tag(
                    'figure',
                    Tag::ALLOW_CHILDREN + Behavior::ENCODE_INVALID_TAG + Behavior::REMOVE_UNEXPECTED_CHILDREN
                ))
                    ->addAttrs(
                        ...$this->globalAttrs
            )
        );
    }

The 'new Tag()' method creates a new tag object. The first parameter here is the desired tag; in our example, 'figure'. The second parameter sets various flags that define the sanitizer’s behavior for this tag.
Calling the 'addAttrs()' method of the tag object also defines the allowed attributes—in this case, all global HTML attributes.

Multiple tags can also be added to the sanitizer in this way:

public function createBehavior(): Behavior
    {
        return parent::createBehavior()
            ->withName('common')
            ->withTags(             
                (new Tag(
                    'select',
                    Tag::ALLOW_CHILDREN
                ))->addAttrs(
                    ...$this->globalAttrs
                ),
                (new Tag(
                    'option',
                    Tag::ALLOW_CHILDREN
                ))->addAttrs(
                    (new Attr('value')),
                    ...$this->globalAttrs
                ),
                // more tags...
            )
        );
    }

In addition to the global attributes already mentioned, in the example above we add a new attribute, "value," to the "option" tag.

To add additional attributes to tags that have already been defined, you can use the 'getTag()' method to retrieve the desired tag from the behavior. In our example, we add the 'loading' attribute to the existing 'img' tag:

public function createBehavior(): Behavior
{
    $behaviour = parent::createBehavior();
 
    // add loading attribute to img tag
    $imgTag = $behaviour->getTag('img');
 
    if ($imgTag !== null) {
        $imgTag->addAttrs(new Attr('loading'));
    }
 
    return $behaviour;
}

Finally, you need to register your own sanitizer in ext_locaconf.php. In our case, we replaced the existing default sanitizer:

// Override HTML sanitizer to allow tags and attributes that we need
$GLOBALS['TYPO3_CONF_VARS']['SYS']['htmlSanitizer']['default'] = PunktDe\PtSite\Sanitizer\CustomHtmlSanitizer::class;

Sanitizer Logging

If you’re not yet sure exactly which elements on your site are no longer functioning due to the use of the sanitizer, we recommend enabling logging. For more information on logging, visit the official HTMLSanitizer documentation.

In our LocalConfiguration.php file in TYPO3 9, the configuration looks like this (7 corresponds to the debug log level):


'LOG' => [
    'TYPO3' => [
        'HtmlSanitizer' => [
            'writerConfiguration' => [
                '7' => [
                    TYPO3\CMS\Core\Log\Writer\FileWriter::class => [
                        'logFileInfix' => 'html',
                    ],
                ],
            ],
        ],
    ],
],

Once logging is enabled, you'll find a log file on your server containing the sanitizer's messages.
Depending on whether the site was installed using Composer or the classic method, you’ll find the log file in the var/log directory, parallel to the web root directory (Composer mode) or in web-root/typo3temp/var/log (classic mode).

Additional Links

Security Notice: https://typo3.org/article/typo3-core-sa-2021-013

Changelog: https://docs.typo3.org/c/typo3/cms-core/master/en-us/Changelog/9.5.x/Important-94484-IntroduceHTMLSanitizer.html

Bug report regarding missing tags in the sanitizer: https://github.com/TYPO3/html-sanitizer/issues/16#issuecomment-896204718 (It’s worth taking a general look at the open issues in the meantime.)

Share:

More articles

if (sad() === true) { sad().stop() ; beAwesome(); }
André Hoffmann, Entwicklung at punkt.de
Working at punkt.de