{"schema":"threatline.record/2","record":{"id":"CVE-2026-102601","aliases":["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->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","categories":["Libraries & packages"],"product":"league/flysystem","vendor":"thephpleague","ecosystem":"composer","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-cxf4-7mrp-vvpr","source_modified_at":"2026-09-30T21:26:47Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"composer","name":"league/flysystem"},"range":"<= 3.35.2"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-102601","source_modified_at":"2026-09-29T16:02:04.360Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"flysystem","vendor":"thephpleague","versions":[{"version":"< 3.35.3","status":"affected"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-cxf4-7mrp-vvpr","source_modified_at":"2026-09-30T21:30:04.116870239Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"Packagist","name":"league/flysystem","purl":"pkg:composer/league/flysystem"},"ranges":[{"events":[{"introduced":"0"},{"fixed":"3.35.3"}],"type":"ECOSYSTEM"}],"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"]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-cxf4-7mrp-vvpr","source_modified_at":"2026-09-30T21:26:47Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"composer","name":"league/flysystem"},"version":"3.35.3"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-cxf4-7mrp-vvpr","source_modified_at":"2026-09-30T21:30:04.116870239Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"Packagist","name":"league/flysystem","purl":"pkg:composer/league/flysystem"},"version":"3.35.3"}],"cvss":3.5,"severity":"Low","kev":{"listed":false},"epss":{"probability":0.00337,"percentile":0.25905,"score_date":"2026-10-23"},"published_at":"2026-09-29T15:26:42.044Z","modified_at":"2026-09-30T21:30:04.116870239Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-cxf4-7mrp-vvpr","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-09-30T21:26:47Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-102601","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-09-29T16:02:04.360Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-cxf4-7mrp-vvpr","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-09-30T21:30:04.116870239Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-102601","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/thephpleague/flysystem/security/advisories/GHSA-cxf4-7mrp-vvpr","tags":["exploit"]},{"source":"CVE List","url":"https://github.com/thephpleague/flysystem/commit/ef4a9a557d769b5d472c403125716706a0d9cc77","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/thephpleague/flysystem/releases/tag/3.35.3","tags":["x_refsource_MISC"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-102601","source":"GitHub Advisory Database"},{"url":"https://github.com/advisories/GHSA-cxf4-7mrp-vvpr","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"no","technical_impact":"partial","assessed_at":"2026-09-29T16:01:15.275868Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-09-30T21:30:04.116870239Z","content_hash":"0126da1a023e3399a837df361493e43d8531ef05b44479655f9c4ee4e69512c2"}}
