{"id":"GHSA-cxf4-7mrp-vvpr","summary":"Flysystem: WhitespacePathNormalizer's control-character (CorruptedPathDetected) check is bypassed by malformed UTF-8 in the path, affecting every adapter","details":"## Related public issue (context, not a duplicate)\n\nClosed issue #1429 (\"Handle non-UTF-8 paths\", 2024-03-24) raised exactly this general concern and\neven suggested detection via `preg_match('//u', $path) !== 1` -- note the reporter's suggested check\nexplicitly compares `!== 1`, which *would* correctly treat PCRE's `false` return as \"reject.\" The\nmaintainer's reply pointed to the `PathNormalizer` interface as the place to implement this. The\ncontrol-character check that ended up shipping in `WhitespacePathNormalizer`\n(`if (preg_match('#\\p{C}+#u', $unixPath))`) addresses the general concern but does **not** use the\n`!== 1`-style comparison the original issue suggested -- it uses a bare truthy check, which is exactly\nthe gap this report demonstrates. So this is not a duplicate of #1429; it's a concrete bypass\nsurviving in the fix that issue's concern led to.\n\n## Vulnerability Details\n\n**File**: `src/WhitespacePathNormalizer.php`, lines 22-28 (`normalizePath()`) -- the **default**\n`PathNormalizer` used by `Filesystem` for every adapter (Local, FTP, SFTP, S3, AsyncAwsS3, Azure, GCS,\nZipArchive, GridFS, InMemory) unless the application supplies a custom one.\n\n### Root Cause\n\n```php\npublic function normalizePath(string $path): string\n{\n    $unixPath = str_replace('\\\\', '/', $path);\n\n    if (preg_match('#\\p{C}+#u', $unixPath)) {\n        throw CorruptedPathDetected::forPath($path);\n    }\n    ...\n```\n\n`preg_match()` returns `false` (a PHP engine error) rather than `0` when the subject string is not\nvalid UTF-8 and the pattern uses the `/u` modifier -- PCRE can't even attempt the match. `false` and\n`0` are both falsy in PHP, and `if (preg_match(...))` does not distinguish them. So a path containing\n**any single invalid UTF-8 byte anywhere in the string** makes `preg_match()` fail with a \"Malformed\nUTF-8 characters\" engine error, the `if` evaluates false, and `CorruptedPathDetected` is silently **not**\nthrown -- even when the same string also contains literal control characters this exact check exists\nto catch.\n\nthe identical payload IS correctly rejected once it's valid UTF-8:\n```php\n$n-\u003enormalizePath(\"foo\\x1bbar\");         // valid UTF-8, contains ESC -\u003e throws CorruptedPathDetected (correct)\n$n-\u003enormalizePath(\"foo\\x80\\x1bbar\");    // 0x80 = invalid lone UTF-8 continuation byte -\u003e NOT thrown (bypass)\n```\n\nThe path-traversal protection is **unaffected** -- it's exact byte-string comparison on\n`/`-delimited segments, independent of UTF-8 validity:\n```php\n$n-\u003enormalizePath(\"\\x80/../../etc/passwd\");   // still throws PathTraversalDetected\n```\n\n### Recommended Fix\n```php\npublic function normalizePath(string $path): string\n{\n    $unixPath = str_replace('\\\\', '/', $path);\n    $matched = preg_match('#\\p{C}+#u', $unixPath);\n\n    if ($matched !== 0) {\n        // $matched === false means malformed UTF-8 -- must also be treated as corrupted,\n        // not silently allowed through.\n        throw CorruptedPathDetected::forPath($path);\n    }\n    ...\n```\n\n### Verification\nDynamically confirmed on `league/flysystem` HEAD `6837e1d` / tag `3.35.2`, PHP 8.4.22 CLI, end-to-end\nthrough `LocalFilesystemAdapter`:\n```\n[1] write() succeeded -- normalizer did NOT reject the path.\n[2] Actual bytes on disk: ...801b5b386d6e6f726d616c2d6c6f6f6b696e672d66696c652e7478741b5b306d...\n[3] Filesystem::listContents() path contains raw ESC (0x1b): YES\n[4] Raw terminal output (via `cat -v`): M-^@^[[8mnormal-looking-file.txt^[[0m^[[2K^[[1Aurgent-invoice.pdf","aliases":["CVE-2026-102601"],"modified":"2026-09-29T18:15:05.573638261Z","published":"2026-09-29T18:10:14Z","database_specific":{"nvd_published_at":null,"cwe_ids":["CWE-150"],"severity":"LOW","github_reviewed":true,"github_reviewed_at":"2026-09-29T18:10:14Z"},"references":[{"type":"WEB","url":"https://github.com/thephpleague/flysystem/security/advisories/GHSA-cxf4-7mrp-vvpr"},{"type":"WEB","url":"https://github.com/thephpleague/flysystem/commit/ef4a9a557d769b5d472c403125716706a0d9cc77"},{"type":"PACKAGE","url":"https://github.com/thephpleague/flysystem"},{"type":"WEB","url":"https://github.com/thephpleague/flysystem/releases/tag/3.35.3"}],"affected":[{"package":{"name":"league/flysystem","ecosystem":"Packagist","purl":"pkg:composer/league/flysystem"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.35.3"}]}],"versions":["0.1.0","0.1.1","0.1.10","0.1.11","0.1.12","0.1.13","0.1.14","0.1.15","0.1.16","0.1.17","0.1.18","0.1.19","0.1.2","0.1.20","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.1.8","0.1.9","0.2.0","0.2.1","0.2.10","0.2.11","0.2.12","0.2.13","0.2.14","0.2.15","0.2.2","0.2.3","0.2.4","0.2.5","0.2.6","0.2.7","0.2.8","0.2.9","0.3.0","0.3.1","0.3.2","0.3.3","0.3.4","0.3.5","0.4.0","0.4.1","0.4.2","0.4.3","0.4.4","0.4.5","0.5.0","0.5.1","0.5.10","0.5.11","0.5.12","0.5.2","0.5.3","0.5.4","0.5.5","0.5.6","0.5.7","0.5.8","0.5.9","1.0.0","1.0.0-alpha1","1.0.1","1.0.10","1.0.11","1.0.12","1.0.13","1.0.14","1.0.15","1.0.16","1.0.17","1.0.18","1.0.19","1.0.2","1.0.20","1.0.21","1.0.22","1.0.23","1.0.24","1.0.25","1.0.26","1.0.27","1.0.28","1.0.29","1.0.3","1.0.30","1.0.31","1.0.32","1.0.33","1.0.34","1.0.35","1.0.36","1.0.37","1.0.38","1.0.39","1.0.4","1.0.40","1.0.41","1.0.42","1.0.43","1.0.44","1.0.45","1.0.46","1.0.47","1.0.48","1.0.49","1.0.5","1.0.50","1.0.51","1.0.52","1.0.53","1.0.54","1.0.55","1.0.56","1.0.57","1.0.58","1.0.59","1.0.6","1.0.60","1.0.61","1.0.62","1.0.63","1.0.64","1.0.65","1.0.66","1.0.67","1.0.68","1.0.69","1.0.7","1.0.70","1.0.8","1.0.9","1.1.0","1.1.1","1.1.10","1.1.2","1.1.3","1.1.4","1.1.5","1.1.6","1.1.7","1.1.8","1.1.9","2.0.0","2.0.0-RC1","2.0.0-alpha.1","2.0.0-alpha.2","2.0.0-alpha.3","2.0.0-alpha.4","2.0.0-beta.1","2.0.0-beta.2","2.0.0-beta.3","2.0.1","2.0.2","2.0.3","2.0.4","2.0.5","2.0.6","2.0.7","2.0.8","2.1.0","2.1.1","2.2.0","2.2.1","2.2.2","2.2.3","2.3.0","2.3.1","2.3.2","2.4.0","2.4.1","2.4.2","2.4.3","2.4.4","2.4.5","2.5.0","3.0.0","3.0.1","3.0.10","3.0.11","3.0.12","3.0.13","3.0.14","3.0.15","3.0.16","3.0.17","3.0.18","3.0.19","3.0.2","3.0.20","3.0.21","3.0.22","3.0.23","3.0.3","3.0.4","3.0.5","3.0.6","3.0.7","3.0.8","3.0.9","3.1.0","3.1.1","3.10.0","3.10.1","3.10.2","3.10.3","3.10.4","3.11.0","3.12.0","3.12.1","3.12.2","3.12.3","3.13.0","3.14.0","3.15.0","3.15.1","3.16.0","3.17.0","3.18.0","3.19.0","3.2.0","3.2.1","3.20.0","3.21.0","3.22.0","3.23.0","3.23.1","3.24.0","3.25.0","3.25.1","3.26.0","3.27.0","3.28.0","3.29.0","3.29.1","3.3.0","3.30.0","3.30.1","3.30.2","3.31.0","3.32.0","3.33.0","3.34.0","3.35.0","3.35.1","3.35.2","3.4.0","3.5.0","3.5.1","3.5.2","3.6.0","3.7.0","3.8.0","3.9.0"],"database_specific":{"last_known_affected_version_range":"\u003c= 3.35.2","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-cxf4-7mrp-vvpr/GHSA-cxf4-7mrp-vvpr.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N"}]}