From ec7617fbde9910e8265d27797c476305d31919ea Mon Sep 17 00:00:00 2001
From: Oliver Hader <oliver@typo3.org>
Date: Wed, 5 Jul 2023 19:44:46 +0200
Subject: [PATCH] [BUGFIX] Normalize filename of uploaded files
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The filename of uploaded files might not be encoded
as normalized unicode. For instance, this happens when
using umlauts in filenames on HFS+ filesystem (macOS).

For instance the client sends an `ö`, which is sent in
NFD as `0x6fcc88`, but should be normalized as `0xc3b6`.

https://en.wikipedia.org/wiki/Unicode_equivalence#Normalization

Executed commands:
composer req symfony/polyfill-intl-normalizer:^1.27
composer req symfony/polyfill-intl-normalizer:^1.27 \
    -d typo3/sysext/core --no-update

Resolves: #101253
Releases: main, 12.4, 11.5
Change-Id: I8605481ffdc3b5d96f529850bf09a1fd75d09cd2
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/79837
Tested-by: core-ci <typo3@b13.com>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
---
 composer.json                                         |  1 +
 composer.lock                                         |  2 +-
 typo3/sysext/core/Classes/Http/UploadedFile.php       |  5 ++++-
 .../sysext/core/Classes/Resource/ResourceStorage.php  |  2 +-
 .../core/Classes/Utility/File/ExtendedFileUtility.php |  1 +
 .../sysext/core/Tests/Unit/Http/UploadedFileTest.php  | 11 +++++++++++
 typo3/sysext/core/composer.json                       |  1 +
 7 files changed, 20 insertions(+), 3 deletions(-)

diff --git a/composer.json b/composer.json
index afc5753b3f95..63245129bfbc 100644
--- a/composer.json
+++ b/composer.json
@@ -87,6 +87,7 @@
 		"symfony/messenger": "^6.2",
 		"symfony/mime": "^6.2",
 		"symfony/options-resolver": "^6.2",
+		"symfony/polyfill-intl-normalizer": "^1.27",
 		"symfony/property-access": "^6.2",
 		"symfony/property-info": "^6.2.11",
 		"symfony/rate-limiter": "^6.2",
diff --git a/composer.lock b/composer.lock
index 5263cc7e860a..032e3a252b48 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": "e692c4b3082cbc8207fa2e0897d31ca9",
+    "content-hash": "7396b099be92db1ca53986a033f316ff",
     "packages": [
         {
             "name": "bacon/bacon-qr-code",
diff --git a/typo3/sysext/core/Classes/Http/UploadedFile.php b/typo3/sysext/core/Classes/Http/UploadedFile.php
index 6f1da82864f4..99f3b5013c84 100644
--- a/typo3/sysext/core/Classes/Http/UploadedFile.php
+++ b/typo3/sysext/core/Classes/Http/UploadedFile.php
@@ -74,7 +74,10 @@ class UploadedFile implements UploadedFileInterface
         }
         $this->error = $errorStatus;
 
-        $this->clientFilename = $clientFilename;
+        if ($clientFilename !== null) {
+            $clientFilename = \Normalizer::normalize($clientFilename);
+        }
+        $this->clientFilename = is_string($clientFilename) ? $clientFilename : null;
         $this->clientMediaType = $clientMediaType;
     }
 
diff --git a/typo3/sysext/core/Classes/Resource/ResourceStorage.php b/typo3/sysext/core/Classes/Resource/ResourceStorage.php
index 8c6879412b2f..bdfcafa48295 100644
--- a/typo3/sysext/core/Classes/Resource/ResourceStorage.php
+++ b/typo3/sysext/core/Classes/Resource/ResourceStorage.php
@@ -2098,7 +2098,7 @@ class ResourceStorage implements ResourceStorageInterface
         } else {
             $localFilePath = $uploadedFileData['tmp_name'];
             if ($targetFileName === null) {
-                $targetFileName = $uploadedFileData['name'];
+                $targetFileName = \Normalizer::normalize($uploadedFileData['name']);
             }
             $size = $uploadedFileData['size'];
         }
diff --git a/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php b/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
index 8291fa248930..ea30d53b89d7 100644
--- a/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
+++ b/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
@@ -1049,6 +1049,7 @@ class ExtendedFileUtility extends BasicFileUtility
                 'size' => [$uploadedFileData['size']],
             ];
         }
+        $uploadedFileData['name'] = array_map(\Normalizer::normalize(...), $uploadedFileData['name']);
         $resultObjects = [];
         $numberOfUploadedFilesForPosition = count($uploadedFileData['name']);
         // Loop through all uploaded files
diff --git a/typo3/sysext/core/Tests/Unit/Http/UploadedFileTest.php b/typo3/sysext/core/Tests/Unit/Http/UploadedFileTest.php
index c24c399381bc..517eebaa70ab 100644
--- a/typo3/sysext/core/Tests/Unit/Http/UploadedFileTest.php
+++ b/typo3/sysext/core/Tests/Unit/Http/UploadedFileTest.php
@@ -180,4 +180,15 @@ final class UploadedFileTest extends UnitTestCase
         $this->expectExceptionCode(1436717306);
         $upload->getStream();
     }
+
+    /**
+     * see https://en.wikipedia.org/wiki/Unicode_equivalence#Normalization, "NFD"
+     * @test
+     */
+    public function nfdFileNameIsNormalized(): void
+    {
+        $clientFileName = hex2bin('6fcc88') . '.png';
+        $subject = new UploadedFile(fopen('php://temp', 'wb+'), 0, 0, $clientFileName);
+        self::assertSame(hex2bin('c3b6') . '.png', $subject->getClientFilename());
+    }
 }
diff --git a/typo3/sysext/core/composer.json b/typo3/sysext/core/composer.json
index a60489d95098..294a0358f623 100644
--- a/typo3/sysext/core/composer.json
+++ b/typo3/sysext/core/composer.json
@@ -66,6 +66,7 @@
 		"symfony/messenger": "^6.2",
 		"symfony/mime": "^6.2",
 		"symfony/options-resolver": "^6.2",
+		"symfony/polyfill-intl-normalizer": "^1.27",
 		"symfony/rate-limiter": "^6.2",
 		"symfony/routing": "^6.2",
 		"symfony/uid": "^6.2",
-- 
GitLab