{"id":"CVE-2026-102601","aliases":["GHSA-cxf4-7mrp-vvpr"],"url":"https://o3.security/vulnerability/CVE-2026-102601","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->normalizePath(\"foo\\x1bbar\");         // valid UTF-8, contains ESC -> throws CorruptedPathDetected (correct)\n$n->normalizePath(\"foo\\x80\\x1bbar\");    // 0x80 = invalid lone UTF-8 continuation byte -> 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->normalizePath(\"\\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","published":"2026-09-29T15:26:42.044Z","modified":"2026-10-21T02:55:09.352925762Z","cvss":{"score":3.5,"severity":"LOW","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Packagist","name":"league/flysystem","fixedVersion":"3.35.3"}],"fix":{"url":"https://github.com/thephpleague/flysystem/commit/ef4a9a557d769b5d472c403125716706a0d9cc77","label":"thephpleague/flysystem@ef4a9a5"},"references":[{"type":"WEB","url":"https://github.com/thephpleague/flysystem/releases/tag/3.35.3"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/102xxx/CVE-2026-102601.json"},{"type":"ADVISORY","url":"https://github.com/thephpleague/flysystem/security/advisories/GHSA-cxf4-7mrp-vvpr"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-102601"},{"type":"FIX","url":"https://github.com/thephpleague/flysystem/commit/ef4a9a557d769b5d472c403125716706a0d9cc77"},{"type":"PACKAGE","url":"https://github.com/thephpleague/flysystem"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-21T02:55:09.352925762Z"}}