diff --git a/composer.json b/composer.json
index da8c5f7f5182..64aea6282908 100644
--- a/composer.json
+++ b/composer.json
@@ -44,6 +44,7 @@
"doctrine/instantiator": "^1.1",
"doctrine/lexer": "^1.0",
"egulias/email-validator": "^2.1",
+ "enshrined/svg-sanitize": "^0.14.0",
"guzzlehttp/guzzle": "^6.3.0",
"guzzlehttp/psr7": "^1.4.0",
"nikic/php-parser": "^4.10.4",
diff --git a/composer.lock b/composer.lock
index 41142e064184..293288f8fb89 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "664bdaf86dd7273cd840fa7eaa998893",
+ "content-hash": "a359b4a71f0ba72147d893bb1fd8278c",
"packages": [
{
"name": "cogpowered/finediff",
@@ -617,6 +617,51 @@
},
"time": "2020-02-13T22:36:52+00:00"
},
+ {
+ "name": "enshrined/svg-sanitize",
+ "version": "0.14.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/darylldoyle/svg-sanitizer.git",
+ "reference": "beff89576a72540ee99476aeb9cfe98222e76fb8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/beff89576a72540ee99476aeb9cfe98222e76fb8",
+ "reference": "beff89576a72540ee99476aeb9cfe98222e76fb8",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*"
+ },
+ "require-dev": {
+ "codeclimate/php-test-reporter": "^0.1.2",
+ "phpunit/phpunit": "^6"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "enshrined\\svgSanitize\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Daryll Doyle",
+ "email": "daryll@enshrined.co.uk"
+ }
+ ],
+ "description": "An SVG sanitizer for PHP",
+ "support": {
+ "issues": "https://github.com/darylldoyle/svg-sanitizer/issues",
+ "source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.14.0"
+ },
+ "time": "2021-01-21T10:13:20+00:00"
+ },
{
"name": "guzzlehttp/guzzle",
"version": "6.4.1",
@@ -7528,5 +7573,5 @@
"platform-overrides": {
"php": "7.2.5"
},
- "plugin-api-version": "2.0.0"
+ "plugin-api-version": "2.1.0"
}
diff --git a/typo3/sysext/core/Classes/Resource/Security/SvgEventListener.php b/typo3/sysext/core/Classes/Resource/Security/SvgEventListener.php
new file mode 100644
index 000000000000..41ddf496da59
--- /dev/null
+++ b/typo3/sysext/core/Classes/Resource/Security/SvgEventListener.php
@@ -0,0 +1,72 @@
+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);
+ }
+ }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Security/SvgHookHandler.php b/typo3/sysext/core/Classes/Resource/Security/SvgHookHandler.php
new file mode 100644
index 000000000000..26f050c9e5ef
--- /dev/null
+++ b/typo3/sysext/core/Classes/Resource/Security/SvgHookHandler.php
@@ -0,0 +1,48 @@
+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);
+ }
+ }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Security/SvgSanitizer.php b/typo3/sysext/core/Classes/Resource/Security/SvgSanitizer.php
new file mode 100644
index 000000000000..373ade783bf1
--- /dev/null
+++ b/typo3/sysext/core/Classes/Resource/Security/SvgSanitizer.php
@@ -0,0 +1,56 @@
+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) ?: '';
+ }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Security/SvgTypeCheck.php b/typo3/sysext/core/Classes/Resource/Security/SvgTypeCheck.php
new file mode 100644
index 000000000000..66509f620564
--- /dev/null
+++ b/typo3/sysext/core/Classes/Resource/Security/SvgTypeCheck.php
@@ -0,0 +1,76 @@
+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)) : [];
+ }
+}
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
index 3441b92e39f7..3d4b023197fc 100644
--- a/typo3/sysext/core/Configuration/Services.yaml
+++ b/typo3/sysext/core/Configuration/Services.yaml
@@ -342,6 +342,27 @@ services:
event: TYPO3\CMS\Core\Resource\Event\AfterFileDeletedEvent
+ 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:
diff --git a/typo3/sysext/core/Documentation/Changelog/9.5.x/Important-94492-IntroduceSVGSanitizer.rst b/typo3/sysext/core/Documentation/Changelog/9.5.x/Important-94492-IntroduceSVGSanitizer.rst
new file mode 100644
index 000000000000..60813267c3df
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/9.5.x/Important-94492-IntroduceSVGSanitizer.rst
@@ -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
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/TYPO3_Logo_Clean.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/TYPO3_Logo_Clean.svg
new file mode 100644
index 000000000000..01db9ad346d1
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/TYPO3_Logo_Clean.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/TYPO3_Logo_Data.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/TYPO3_Logo_Data.svg
new file mode 100644
index 000000000000..4e4449894aa2
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/TYPO3_Logo_Data.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/TYPO3_Logo_Script.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/TYPO3_Logo_Script.svg
new file mode 100644
index 000000000000..01db9ad346d1
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/TYPO3_Logo_Script.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/ariaData.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/ariaData.svg
new file mode 100644
index 000000000000..838e7dc3df9b
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/ariaData.svg
@@ -0,0 +1,57 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/billion_laughs.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/billion_laughs.svg
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/entity.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/entity.svg
new file mode 100644
index 000000000000..a7c49b4d49d2
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/entity.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/entity_2.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/entity_2.svg
new file mode 100644
index 000000000000..cf314616c2ab
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/entity_2.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/external.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/external.svg
new file mode 100644
index 000000000000..ef4ab4114867
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/external.svg
@@ -0,0 +1,10 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/hrefOne.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/hrefOne.svg
new file mode 100644
index 000000000000..b84d2184df75
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/hrefOne.svg
@@ -0,0 +1,13 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/hrefTwo.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/hrefTwo.svg
new file mode 100644
index 000000000000..2a95507281f2
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/hrefTwo.svg
@@ -0,0 +1,13 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/html.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/html.svg
new file mode 100644
index 000000000000..c524480099fc
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/html.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/simple.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/simple.svg
new file mode 100644
index 000000000000..c524480099fc
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/simple.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/svgOne.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/svgOne.svg
new file mode 100644
index 000000000000..da8faf5f0702
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/svgOne.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/use.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/use.svg
new file mode 100644
index 000000000000..badd6870ba3c
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/use.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/useDos.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/useDos.svg
new file mode 100644
index 000000000000..bacbafc2c1fb
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/useDos.svg
@@ -0,0 +1,113 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xlinkLaughs.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xlinkLaughs.svg
new file mode 100644
index 000000000000..21661833614a
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xlinkLaughs.svg
@@ -0,0 +1,71 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xlinkLoop.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xlinkLoop.svg
new file mode 100644
index 000000000000..c6dd6bdaff67
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xlinkLoop.svg
@@ -0,0 +1,24 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xlink_laughs.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xlink_laughs.svg
new file mode 100644
index 000000000000..ae53de9ab717
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xlink_laughs.svg
@@ -0,0 +1,146 @@
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xmlOne.xml b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xmlOne.xml
new file mode 100644
index 000000000000..eddcb7b55586
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xmlOne.xml
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xss.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xss.svg
new file mode 100644
index 000000000000..7e8106f2285b
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/CleanSVG/xss.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/TYPO3_Logo_Clean.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/TYPO3_Logo_Clean.svg
new file mode 100644
index 000000000000..01db9ad346d1
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/TYPO3_Logo_Clean.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/TYPO3_Logo_Data.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/TYPO3_Logo_Data.svg
new file mode 100644
index 000000000000..0a014746b779
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/TYPO3_Logo_Data.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/TYPO3_Logo_Script.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/TYPO3_Logo_Script.svg
new file mode 100644
index 000000000000..e1170d4f674d
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/TYPO3_Logo_Script.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/ariaData.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/ariaData.svg
new file mode 100644
index 000000000000..d09ea5d95413
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/ariaData.svg
@@ -0,0 +1,56 @@
+
+ Pixels, My Super-friendly Cat
+ An illustrated gray cat with bright green blinking eyes.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/billion_laughs.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/billion_laughs.svg
new file mode 100644
index 000000000000..dc9414089ff9
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/billion_laughs.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+]>
+
+ &lol10;
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/entity.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/entity.svg
new file mode 100644
index 000000000000..bc2d3defac21
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/entity.svg
@@ -0,0 +1,5 @@
+
+]>
+
+ &lab;
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/entity_2.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/entity_2.svg
new file mode 100644
index 000000000000..19d3a5b057eb
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/entity_2.svg
@@ -0,0 +1,8 @@
+
+
+
+]>
+
+ &lab2;
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/external.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/external.svg
new file mode 100644
index 000000000000..cd190cb20038
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/external.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/hrefOne.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/hrefOne.svg
new file mode 100644
index 000000000000..4f26966a17e5
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/hrefOne.svg
@@ -0,0 +1,13 @@
+
+
+ test 1
+ test 2
+ test 3
+ test 4
+
+ test 5
+ test 6
+
+ test 7
+ test 8
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/hrefTwo.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/hrefTwo.svg
new file mode 100644
index 000000000000..3e7be42d7cf3
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/hrefTwo.svg
@@ -0,0 +1,13 @@
+
+
+ test 1
+ test 2
+ test 3
+ test 4
+
+ test 5
+ test 6
+
+ test 7
+ test 8
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/html.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/html.svg
new file mode 100644
index 000000000000..31bd3590ce8d
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/html.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ HTML Injection for phishing
+
+
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/simple.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/simple.svg
new file mode 100644
index 000000000000..29fc853c7715
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/simple.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/svgOne.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/svgOne.svg
new file mode 100644
index 000000000000..f543a84b5f32
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/svgOne.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+shouldn't be here
+
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/use.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/use.svg
new file mode 100644
index 000000000000..888416a20809
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/use.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/useDos.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/useDos.svg
new file mode 100644
index 000000000000..3a3248ed553e
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/useDos.svg
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xlinkLaughs.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xlinkLaughs.svg
new file mode 100644
index 000000000000..c344a80ad932
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xlinkLaughs.svg
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xlinkLoop.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xlinkLoop.svg
new file mode 100644
index 000000000000..e8de02d74a56
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xlinkLoop.svg
@@ -0,0 +1,31 @@
+
+
+
+
+ Ping
+
+
+
+ Pong
+
+
+
+
+ 1st
+
+
+
+ 2nd
+
+
+
+ 3rd
+
+
+
+ 4th
+
+
+
+
+
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xlink_laughs.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xlink_laughs.svg
new file mode 100644
index 000000000000..01c676a49dfe
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xlink_laughs.svg
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xmlOne.xml b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xmlOne.xml
new file mode 100644
index 000000000000..0566a4500531
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xmlOne.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+ <ΓΈ:script src="//0x.lv/" />
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xss.svg b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xss.svg
new file mode 100644
index 000000000000..f6aa93d242e9
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Fixtures/DirtySVG/xss.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Resource/Security/SvgSanitizerTest.php b/typo3/sysext/core/Tests/Functional/Resource/Security/SvgSanitizerTest.php
new file mode 100644
index 000000000000..109232378c51
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Resource/Security/SvgSanitizerTest.php
@@ -0,0 +1,60 @@
+
+ */
+ public function svgContentIsSanitizedDataProvider(): array
+ {
+ $basePath = dirname(__FILE__, 2) . '/Fixtures/';
+ $finder = new Finder();
+ $finder
+ ->files()
+ ->in($basePath . 'DirtySVG/')
+ ->name('*.svg');
+ $data = [];
+ foreach ($finder as $file) {
+ $fileName = $file->getFilename();
+ $data[$fileName] = ['DirtySVG/' . $fileName, 'CleanSVG/' . $fileName];
+ }
+ return $data;
+ }
+
+ /**
+ * @param string $filePath
+ * @param string $sanitizedFilePath
+ * @test
+ * @dataProvider svgContentIsSanitizedDataProvider
+ */
+ public function svgContentIsSanitized($filePath, $sanitizedFilePath)
+ {
+ $basePath = dirname(__FILE__, 2) . '/Fixtures/';
+ $sanitizer = new SvgSanitizer();
+ self::assertStringEqualsFile(
+ $basePath . $sanitizedFilePath,
+ $sanitizer->sanitizeContent(file_get_contents($basePath . $filePath))
+ );
+ }
+}
diff --git a/typo3/sysext/core/composer.json b/typo3/sysext/core/composer.json
index 4000a3756a1b..37f71087395d 100644
--- a/typo3/sysext/core/composer.json
+++ b/typo3/sysext/core/composer.json
@@ -32,6 +32,7 @@
"doctrine/instantiator": "^1.1",
"doctrine/lexer": "^1.0",
"egulias/email-validator": "^2.1",
+ "enshrined/svg-sanitize": "^0.14.0",
"guzzlehttp/guzzle": "^6.3.0",
"guzzlehttp/psr7": "^1.4.0",
"nikic/php-parser": "^4.10.4",
diff --git a/typo3/sysext/core/ext_localconf.php b/typo3/sysext/core/ext_localconf.php
index 478ea0cf6d1c..2308bdd08c6d 100644
--- a/typo3/sysext/core/ext_localconf.php
+++ b/typo3/sysext/core/ext_localconf.php
@@ -2,6 +2,7 @@
defined('TYPO3_MODE') or die();
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'][] = \TYPO3\CMS\Core\Resource\Security\SvgHookHandler::class . '->processMoveUploadedFile';
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = \TYPO3\CMS\Core\Resource\Security\FileMetadataPermissionsAspect::class;
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = \TYPO3\CMS\Core\Hooks\BackendUserGroupIntegrityCheck::class;
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = \TYPO3\CMS\Core\Hooks\BackendUserPasswordCheck::class;
diff --git a/typo3/sysext/install/Classes/Updates/SvgFilesSanitization.php b/typo3/sysext/install/Classes/Updates/SvgFilesSanitization.php
new file mode 100644
index 000000000000..4c0b8823d5ba
--- /dev/null
+++ b/typo3/sysext/install/Classes/Updates/SvgFilesSanitization.php
@@ -0,0 +1,200 @@
+storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
+ $this->confirmation = new Confirmation(
+ 'Continue sanitizing SVG files?',
+ $this->getDescription(),
+ false,
+ 'sanitize, backup available',
+ 'cancel',
+ false
+ );
+ }
+
+ /**
+ * Return the identifier for this wizard
+ * This should be the same string as used in the ext_localconf class registration
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ // needs to be static for exact reference
+ return 'TYPO3\CMS\Install\Updates\SvgFilesSanitization';
+ }
+
+ /**
+ * Return the speaking name of this wizard
+ *
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return 'Sanitize existing SVG files in fileadmin folder';
+ }
+
+ /**
+ * Return the description for this wizard
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return 'This upgrade wizard will sanitize all SVG files located in local file storages. '
+ . 'It is very likely that file contents will be changed.' . "\n"
+ . 'Before continuing, please ensure a proper backup of *.svg and *.svgz files is in place before continuing.';
+ }
+
+ /**
+ * Is an update necessary?
+ *
+ * Is used to determine whether a wizard needs to be run.
+ * Check if data for migration exists.
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ foreach ($this->resolveLocalStorages() as $storage) {
+ try {
+ $svgFiles = $this->resolveSvgFiles($storage);
+ } catch (InsufficientFolderAccessPermissionsException $exception) {
+ continue;
+ }
+ if (count($svgFiles) > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Execute the update
+ *
+ * Called when a wizard reports that an update is necessary
+ *
+ * @return bool
+ */
+ public function executeUpdate(): bool
+ {
+ return $this->processSvgFiles();
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ *
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [];
+ }
+
+ /**
+ * Return a confirmation message instance
+ *
+ * @return Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return $this->confirmation;
+ }
+
+ /**
+ * @return ResourceStorage[]
+ */
+ protected function resolveLocalStorages(): array
+ {
+ return array_filter(
+ $this->storageRepository->findByStorageType('Local'),
+ function (ResourceStorage $storage) {
+ return $storage->isWritable();
+ }
+ );
+ }
+
+ /**
+ * @param ResourceStorage $storage
+ * @return File[]
+ * @throws InsufficientFolderAccessPermissionsException
+ */
+ protected function resolveSvgFiles(ResourceStorage $storage): array
+ {
+ $filter = GeneralUtility::makeInstance(FileExtensionFilter::class);
+ $filter->setAllowedFileExtensions(['svg', 'svgz']);
+ return $storage
+ ->setFileAndFolderNameFilters([[$filter, 'filterFileList']])
+ ->getFilesInFolder(
+ $storage->getRootLevelFolder(),
+ 0,
+ 0,
+ true,
+ true
+ );
+ }
+
+ protected function processSvgFiles(): bool
+ {
+ $successful = true;
+ $sanitizer = GeneralUtility::makeInstance(SvgSanitizer::class);
+ foreach ($this->resolveLocalStorages() as $storage) {
+ try {
+ $svgFiles = $this->resolveSvgFiles($storage);
+ } catch (InsufficientFolderAccessPermissionsException $exception) {
+ // @todo Add notice/warning for this upgrade process
+ $successful = false;
+ continue;
+ }
+ foreach ($svgFiles as $svgFile) {
+ $oldFileContent = $svgFile->getContents();
+ $newFileContent = $sanitizer->sanitizeContent($oldFileContent);
+ if ($oldFileContent !== $newFileContent) {
+ $svgFile->setContents($newFileContent);
+ }
+ }
+ }
+ return $successful;
+ }
+}
diff --git a/typo3/sysext/install/ext_localconf.php b/typo3/sysext/install/ext_localconf.php
index bcbea357e9b6..5ec75ef386ed 100644
--- a/typo3/sysext/install/ext_localconf.php
+++ b/typo3/sysext/install/ext_localconf.php
@@ -45,6 +45,8 @@
= \TYPO3\CMS\Install\Updates\TaskcenterExtractionUpdate::class;
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['sysActionExtension']
= \TYPO3\CMS\Install\Updates\SysActionExtractionUpdate::class;
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['svgFilesSanitization']
+ = \TYPO3\CMS\Install\Updates\SvgFilesSanitization::class;
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['databaseRowsUpdateWizard']
= \TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard::class;