Skip to content

Commit

Permalink
[TASK] Introduce SVG Sanitizer
Browse files Browse the repository at this point in the history
This change introduces behavior of extension `t3g/svg-sanitizer` into
the TYPO3 core. Sanitizing SVG data is actually done by external package
`enshrined/svg-sanitize` by Daryll Doyle.

The following aspects are introduced:
+ handle `GeneralUtility::upload_copy_move` invocations
+ handle FAL action events `file-add`, `file-replace`, `set-content`
+ provide upgrade wizard, sanitizing all SVG files in storages that
  are using `LocalDriver`

Custom usage:
```
$sanitizer = new \TYPO3\CMS\Core\Resource\Security\SvgSanitizer();
$sanitizer->sanitizeFile($sourcePath, $targetPath);
$svg = $sanitizer->sanitizeContent($svg);
```

Basically this change enforces following public service announcements
concerning SVG files, to enhance these security aspects per default:
+ https://typo3.org/security/advisory/typo3-psa-2020-003
+ https://typo3.org/security/advisory/typo3-psa-2019-010

Resolves: #94492
Releases: master, 10.4, 9.5
Change-Id: I42c206190d8a335ebaf77b7e5d57b383e3bcbae1
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/69809
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
  • Loading branch information
ohader committed Jul 12, 2021
1 parent 61030c7 commit c506e6f
Show file tree
Hide file tree
Showing 53 changed files with 1,803 additions and 1 deletion.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"doctrine/instantiator": "^1.4",
"doctrine/lexer": "^1.2.1",
"egulias/email-validator": "^2.1",
"enshrined/svg-sanitize": "^0.14.0",
"guzzlehttp/guzzle": "^7.2",
"guzzlehttp/psr7": "^1.7.0",
"nikic/php-parser": "^4.10.4",
Expand Down
47 changes: 46 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions typo3/sysext/core/Classes/Resource/Security/SvgEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?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\Resource\Security;

use TYPO3\CMS\Core\Resource\Event\AfterFileContentsSetEvent;
use TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent;
use TYPO3\CMS\Core\Resource\Event\BeforeFileReplacedEvent;

class SvgEventListener
{
/**
* @var SvgSanitizer
*/
protected $sanitizer;

/**
* @var SvgTypeCheck
*/
protected $typeCheck;

public function __construct(SvgSanitizer $sanitizer, SvgTypeCheck $typeCheck)
{
$this->sanitizer = $sanitizer;
$this->typeCheck = $typeCheck;
}

public function beforeFileAdded(BeforeFileAddedEvent $event): void
{
$filePath = $event->getSourceFilePath();
if ($this->typeCheck->forFilePath($filePath)) {
$this->sanitizer->sanitizeFile($filePath);
}
}

public function beforeFileReplaced(BeforeFileReplacedEvent $event): void
{
$filePath = $event->getLocalFilePath();
if ($this->typeCheck->forFilePath($filePath)) {
$this->sanitizer->sanitizeFile($filePath);
}
}

public function afterFileContentsSet(AfterFileContentsSetEvent $event): void
{
$file = $event->getFile();
if (!$this->typeCheck->forResource($file)) {
return;
}
$content = $event->getContent();
$sanitizedContent = $this->sanitizer->sanitizeContent($content);
// cave: setting content will trigger calling this handler again
// (having custom-flags on `FileInterface` would allow to mark it as "processed")
if ($sanitizedContent !== $content) {
$file->setContents($sanitizedContent);
}
}
}
48 changes: 48 additions & 0 deletions typo3/sysext/core/Classes/Resource/Security/SvgHookHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?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\Resource\Security;

class SvgHookHandler
{
/**
* @var SvgSanitizer
*/
protected $sanitizer;

/**
* @var SvgTypeCheck
*/
protected $typeCheck;

public function __construct(SvgSanitizer $sanitizer, SvgTypeCheck $typeCheck)
{
$this->sanitizer = $sanitizer;
$this->typeCheck = $typeCheck;
}

/**
* @param array $parameters
*/
public function processMoveUploadedFile(array $parameters)
{
$filePath = $parameters['source'] ?? null;
if ($filePath !== null && $this->typeCheck->forFilePath($filePath)) {
$this->sanitizer->sanitizeFile($filePath);
}
}
}
56 changes: 56 additions & 0 deletions typo3/sysext/core/Classes/Resource/Security/SvgSanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?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\Resource\Security;

use enshrined\svgSanitize\Sanitizer;

class SvgSanitizer
{
/**
* @param string $sourcePath
* @param string|null $targetPath
* @throws \BadFunctionCallException
*/
public function sanitizeFile(string $sourcePath, string $targetPath = null): void
{
if ($targetPath === null) {
$targetPath = $sourcePath;
}
$svg = file_get_contents($sourcePath);
if (!is_string($svg)) {
return;
}
$sanitizedSvg = $this->sanitizeContent($svg);
if ($sanitizedSvg !== $svg) {
file_put_contents($targetPath, $sanitizedSvg);
}
}

/**
* @param string $svg
*
* @return string
* @throws \BadFunctionCallException
*/
public function sanitizeContent(string $svg): string
{
$sanitizer = new Sanitizer();
$sanitizer->removeRemoteReferences(true);
return $sanitizer->sanitize($svg) ?: '';
}
}
76 changes: 76 additions & 0 deletions typo3/sysext/core/Classes/Resource/Security/SvgTypeCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?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\Resource\Security;

use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\MimeTypeDetector;
use TYPO3\CMS\Core\Type\File\FileInfo;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class SvgTypeCheck
{
protected const MIME_TYPES = ['image/svg', 'image/svg+xml', 'application/svg', 'application/svg+xml'];

/**
* @var MimeTypeDetector
*/
protected $mimeTypeDetector;

/**
* @var string[]
*/
protected $fileExtensions;

public function __construct(MimeTypeDetector $mimeTypeDetector)
{
$this->mimeTypeDetector = $mimeTypeDetector;
$this->fileExtensions = $this->resolveFileExtensions();
}

public function forFilePath(string $filePath): bool
{
$fileInfo = GeneralUtility::makeInstance(FileInfo::class, $filePath);
$fileExtension = $fileInfo->getExtension();
$mimeType = $fileInfo->getMimeType();
return in_array($fileExtension, $this->fileExtensions, true)
|| in_array($mimeType, self::MIME_TYPES, true);
}

public function forResource(FileInterface $file): bool
{
$fileExtension = $file->getExtension();
$mimeType = $file->getMimeType();
return in_array($fileExtension, $this->fileExtensions, true)
|| in_array($mimeType, self::MIME_TYPES, true);
}

/**
* @return string[]
*/
protected function resolveFileExtensions(): array
{
$fileExtensions = array_map(
function (string $mimeType): array {
return $this->mimeTypeDetector->getFileExtensionsForMimeType($mimeType);
},
self::MIME_TYPES
);
$fileExtensions = array_filter($fileExtensions);
return count($fileExtensions) > 0 ? array_unique(array_merge(...$fileExtensions)) : [];
}
}
21 changes: 21 additions & 0 deletions typo3/sysext/core/Configuration/Services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,27 @@ services:
identifier: 'synchronize-filemounts-after-folder-renamed'
method: 'synchronizeFilemountsAfterRename'

TYPO3\CMS\Core\Resource\Security\SvgEventListener:
tags:
- name: event.listener
identifier: 'svg-resource-storage-listener-before-file-added'
method: 'beforeFileAdded'
event: TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent
- name: event.listener
identifier: 'svg-resource-storage-listener-before-file-replaced'
method: 'beforeFileReplaced'
event: TYPO3\CMS\Core\Resource\Event\BeforeFileReplacedEvent
- name: event.listener
identifier: 'svg-resource-storage-listener-after-file-content-set'
method: 'afterFileContentsSet'
event: TYPO3\CMS\Core\Resource\Event\AfterFileContentsSetEvent

TYPO3\CMS\Core\Resource\Security\SvgHookHandler:
public: true

TYPO3\CMS\Core\Resource\Security\SvgTypeCheck:
public: true

# Core caches, cache.core and cache.assets are injected as early
# entries in TYPO3\CMS\Core\Core\Bootstrap and therefore omitted here
cache.hash:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.. include:: ../../Includes.txt

===========================================
Important: #94492 - Introduce SVG Sanitizer
===========================================

See :issue:`94492`

Description
===========

SVG sanitization behavior of extension [`t3g/svg-sanitizer`](https://packagist.org/packages/t3g/svg-sanitizer)
has been introduced into TYPO3 core. Actual processing is done by low-level sanitization package
[`enshrined/svg-sanitize`](https://packagist.org/packages/enshrined/svg-sanitize) by Daryll Doyle.

Introduced aspects
------------------

* handle :php:`GeneralUtility::upload_copy_move` invocations
* handle FAL action events `file-add`, `file-replace`, `set-content`
* provide upgrade wizard, sanitizing all SVG files in storages that
are using :php:`\TYPO3\CMS\Core\Resource\Driver\LocalDriver`

Custom usage
------------

.. code-block:: php
$sanitizer = new \TYPO3\CMS\Core\Resource\Security\SvgSanitizer();
$sanitizer->sanitizeFile($sourcePath, $targetPath);
$svg = $sanitizer->sanitizeContent($svg);
Basically this change enforces following public service announcements
concerning SVG files, to enhance these security aspects per default:

* [TYPO3-PSA-2020-003: Mitigation of Cross-Site Scripting Vulnerabilities in File Upload Handling](https://typo3.org/security/advisory/typo3-psa-2020-003)
* [TYPO3-PSA-2019-010: Cross-Site Scripting Vulnerabilities in File Upload Handling](https://typo3.org/security/advisory/typo3-psa-2019-010)

.. index:: Backend, FAL, Frontend, ext:core
Loading

0 comments on commit c506e6f

Please sign in to comment.