CVE-2021-32708: Time-of-check Time-of-use (TOCTOU) Race Condition (CWE-367)
value: Flysystem is an open source file storage library for PHP. The whitespace normalisation using in 1.x and 2.x removes any unicode whitespace. Under certain specific conditions this could potentially allow a malicious user to execute code remotely. The conditions are: A user is allowed to supply the path or filename of an uploaded file, the supplied path or filename is not checked against unicode chars, the supplied pathname checked against an extension deny-list, not an allow-list, the supplied path or filename contains a unicode whitespace char in the extension, the uploaded file is stored in a directory that allows PHP code to be executed. Given these conditions are met a user can upload and execute arbitrary code on the system under attack. The unicode whitespace removal has been replaced with a rejection (exception). For 1.x users, upgrade to 1.1.4. For 2.x users, upgrade to 2.1.1.
CWE description: The product checks the state of a resource before using that resource, but the resource's state can change between the check and the use in a way that invalidates the results of the check. This can cause the product to perform invalid actions when the resource is in an unexpected state.
CVSS v3 base score: 8.1
Published: 2021-06-24T17:15Z
Repository: https://github.com/thephpleague/flysystem
Language: PHP
Files changed: src/CorruptedPathDetected.php, src/WhitespacePathNormalizer.php, src/WhitespacePathNormalizerTest.php
Commit date: 2021-06-24 00:07:10 +0200
Commit message: Reject funky whitspace paths.

Diff: diff --git a/src/CorruptedPathDetected.php b/src/CorruptedPathDetected.php
new file mode 100644
index 0000000..70631cc
--- /dev/null
+++ b/src/CorruptedPathDetected.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace League\Flysystem;
+
+use RuntimeException;
+
+final class CorruptedPathDetected extends RuntimeException implements FilesystemException
+{
+    public static function forPath(string $path): CorruptedPathDetected
+    {
+        return new CorruptedPathDetected("Corrupted path detected: " . $path);
+    }
+}
diff --git a/src/WhitespacePathNormalizer.php b/src/WhitespacePathNormalizer.php
index 83e9d3e..135b22a 100644
--- a/src/WhitespacePathNormalizer.php
+++ b/src/WhitespacePathNormalizer.php
@@ -7,25 +7,20 @@ namespace League\Flysystem;
 class WhitespacePathNormalizer implements PathNormalizer
 {
     public function normalizePath(string $path): string
     {
         $path = str_replace('\\', '/', $path);
-        $path = $this->removeFunkyWhiteSpace($path);
+        $this->rejectFunkyWhiteSpace($path);
 
         return $this->normalizeRelativePath($path);
     }
 
-    private function removeFunkyWhiteSpace(string $path): string
+    private function rejectFunkyWhiteSpace(string $path): void
     {
-        // Remove unprintable characters and invalid unicode characters.
-        // We do this check in a loop, since removing invalid unicode characters
-        // can lead to new characters being created.
-        while (preg_match('#\p{C}+|^\./#u', $path)) {
-            $path = (string) preg_replace('#\p{C}+|^\./#u', '', $path);
+        if (preg_match('#\p{C}+#u', $path)) {
+            throw CorruptedPathDetected::forPath($path);
         }
-
-        return $path;
     }
 
     private function normalizeRelativePath(string $path): string
     {
         $parts = [];
diff --git a/src/WhitespacePathNormalizerTest.php b/src/WhitespacePathNormalizerTest.php
index 62c185a..a0288f7 100644
--- a/src/WhitespacePathNormalizerTest.php
+++ b/src/WhitespacePathNormalizerTest.php
@@ -53,11 +53,10 @@ class WhitespacePathNormalizerTest extends TestCase
             ['C:\dirname\\\\subdir\\\\\\subsubdir', 'C:/dirname/subdir/subsubdir'],
             ['C:\\\\dirname\subdir\\\\subsubdir', 'C:/dirname/subdir/subsubdir'],
             ['example/path/..txt', 'example/path/..txt'],
             ['\\example\\path.txt', 'example/path.txt'],
             ['\\example\\..\\path.txt', 'path.txt'],
-            ["some\0/path.txt", 'some/path.txt'],
         ];
     }
 
     /**
      * @test
@@ -67,10 +66,25 @@ class WhitespacePathNormalizerTest extends TestCase
     {
         $this->expectException(PathTraversalDetected::class);
         $this->normalizer->normalizePath($input);
     }
 
+    /**
+     * @test
+     * @dataProvider dpFunkyWhitespacePaths
+     */
+    public function rejecting_funky_whitespace(string $path): void
+    {
+        self::expectException(CorruptedPathDetected::class);
+        $this->normalizer->normalizePath($path);
+    }
+
+    public function dpFunkyWhitespacePaths(): iterable
+    {
+        return [["some\0/path.txt"], ["s\x09i.php"]];
+    }
+
     /**
      * @return array<array<string>>
      */
     public function invalidPathProvider(): array
     {

Vulnerable code:         $path = $this->removeFunkyWhiteSpace($path);
    private function removeFunkyWhiteSpace(string $path): string
        // Remove unprintable characters and invalid unicode characters.
        // We do this check in a loop, since removing invalid unicode characters
        // can lead to new characters being created.
        while (preg_match('#\p{C}+|^\./#u', $path)) {
            $path = (string) preg_replace('#\p{C}+|^\./#u', '', $path);

        return $path;
            ["some\0/path.txt", 'some/path.txt'],
Fixed code: <?php

namespace League\Flysystem;

use RuntimeException;

final class CorruptedPathDetected extends RuntimeException implements FilesystemException
{
    public static function forPath(string $path): CorruptedPathDetected
    {
        return new CorruptedPathDetected("Corrupted path detected: " . $path);
    }
}
        $this->rejectFunkyWhiteSpace($path);
    private function rejectFunkyWhiteSpace(string $path): void
        if (preg_match('#\p{C}+#u', $path)) {
            throw CorruptedPathDetected::forPath($path);
    /**
     * @test
     * @dataProvider dpFunkyWhitespacePaths
     */
    public function rejecting_funky_whitespace(string $path): void
    {
        self::expectException(CorruptedPathDetected::class);
        $this->normalizer->normalizePath($path);
    }

    public function dpFunkyWhitespacePaths(): iterable
    {
        return [["some\0/path.txt"], ["s\x09i.php"]];
    }