Feb 17, 2026·5 min read·11 visits
CakePHP's `PaginatorHelper::limitControl()` failed to sanitize query parameter keys when generating hidden input fields to preserve state. By crafting a URL with a malicious parameter name (e.g., `?"> <script>...=1`), an attacker can execute arbitrary JavaScript in the victim's browser. Patched in 5.2.12 and 5.3.1 via strict escaping and integer casting.
A classic tale of 'helpful' framework magic going rogue. The CakePHP PaginatorHelper, in its noble quest to preserve your search filters across pages, inadvertently created a mechanism to reflect malicious HTML via query parameter keys. This Reflected Cross-Site Scripting (XSS) vulnerability affects versions 5.2.10 through 5.3.0, allowing attackers to inject arbitrary JavaScript into the victim's session by simply convincing them to click a link with a malformed query string key.
Pagination is arguably the most boring part of web development. Nobody wakes up excited to write OFFSET and LIMIT SQL clauses. That is why we use frameworks like CakePHP. They handle the drudgery. Specifically, the PaginatorHelper is the unsung hero that renders those "Previous | 1 | 2 | 3 | Next" links and the "Items per page" dropdowns.
But here is the catch: Pagination needs to be stateful. If you search for "users named Steve" and then click "Page 2", the application needs to remember you were searching for "Steve". If it forgets, Page 2 just lists everyone, which defeats the purpose.
To solve this, CakePHP's limitControl() method (which renders the "items per page" dropdown) does something very helpful. It looks at your current URL query parameters (like ?q=Steve&sort=id) and automatically generates hidden HTML input fields for them inside the limit control form. This ensures that when you change the limit from 20 to 50, you don't lose your search context. It's a feature, not a bug... until it becomes a bug.
Developers are trained—beaten, really—into sanitizing values. If we see $_GET['username'], we instinctively reach for sanitization functions before echoing it out. We assume the value is the dirty part. But what about the key?
In PHP, $_GET is an associative array. It has keys and values. The vulnerability in CVE-2026-23643 exists because the generateHiddenFields method inside PaginatorHelper iterated through the query data and trusted the keys. It took the parameter name and shoved it directly into the name attribute of a hidden input field.
Here is the logic flaw: The code assumed that while values might be malicious, parameter names would be safe. But the HTTP protocol doesn't care. You can send a request where the parameter name is an entire JavaScript payload. When CakePHP echoed that key back to the browser to "preserve state," it unknowingly preserved an XSS payload.
Let's look at the crime scene in src/View/Helper/PaginatorHelper.php. The function generateHiddenFields is responsible for the recursion through the query data.
The Vulnerable Code:
// BEFORE: Blindly trusting $key ($fieldName)
foreach ($data as $key => $value) {
$fieldName = $prefix ? $prefix . '[' . $key . ']' : $key;
if (is_array($value)) {
$out .= $this->generateHiddenFields($value, $fieldName);
} else {
// OOPS: $fieldName is injected directly into the HTML attribute
$out .= $this->Form->hidden($fieldName, ['value' => $value]);
}
}If $fieldName contains "><script>alert(1)</script>, the Form->hidden() helper generates:
<input type="hidden" name=""><script>alert(1)</script>" value="...">
The Fix:
The patch is embarrassingly simple, as most XSS fixes are. They wrapped the $fieldName in the h() function (CakePHP's shortcut for htmlspecialchars).
// AFTER: Sanitizing the key
- $out .= $this->Form->hidden($fieldName, ['value' => $value]);
+ $out .= $this->Form->hidden(h($fieldName), ['value' => $value]);They also added a secondary check. The limit parameter itself, which controls how many items are shown, is now explicitly cast to an integer. This kills a secondary vector where the value of the limit itself could have been manipulated.
'value' => $limit !== null ? (int)$limit : null,To exploit this, we don't need fancy headers or race conditions. We just need a browser and a lack of shame. We need to target a page that uses $this->Paginator->limitControl(). This is common in index views/tables.
The Attack Vector:
Recon: Find a paginated list (e.g., /products/index).
Payload Construction: We need to break out of the name="..." attribute. The payload will look like this as a query parameter:
?"><img src=x onerror=alert(document.domain)>=1
Execution: The application receives the request. The PaginatorHelper sees a parameter key of "><img src=x onerror=alert(document.domain)> with a value of 1. It tries to create a hidden field for it.
Resulting HTML:
<form method="get" ...>
<!-- ... other fields ... -->
<input type="hidden" name=""><img src=x onerror=alert(document.domain)>" value="1">
</form>The browser renders the input, sees the closing quote `
It is "only" Reflected XSS, right? In the hierarchy of vulnerabilities, it sits below RCE and SQLi. However, context is king.
CakePHP is a framework often used to build internal business tools, CRMs, and administrative panels—the kind of apps where users have elevated privileges. If an attacker can trick an administrator into clicking a link (via a phishing email claiming "Urgent: Check these paginated logs"), the script executes in the admin's session.
From there, the attacker can:
CAKEPHP session cookie.Because this vulnerability resides in a core helper used globally across the application, every paginated page in the application potentially becomes an attack vector.
If you are running CakePHP 5.2.10 through 5.3.0, you are vulnerable. The fix was released on January 14, 2026.
Primary Mitigation: Run the magic words:
composer update cakephp/cakephpEnsure you land on 5.2.12 or 5.3.1 (or higher).
Manual Mitigation:
If you are stuck on a legacy version and can't upgrade the core (you poor soul), you shouldn't be editing vendor/ files directly. However, you can extend the PaginatorHelper in your src/View/Helper directory, override limitControl, and ensure you aren't passing unescaped data. But honestly? Just upgrade. It's Composer. It usually works. Usually.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
CakePHP Cake Software Foundation | >= 5.2.10, < 5.2.12 | 5.2.12 |
CakePHP Cake Software Foundation | 5.3.0 | 5.3.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 (Cross-site Scripting) |
| CVSS v3.1 | 5.4 (Medium) |
| Attack Vector | Network (Reflected via Query String) |
| Impact | Session Hijacking, DOM Manipulation |
| EPSS Score | 0.00043 (Low Probability) |
| Exploit Status | No Public Weaponized Exploit |
| KEV Status | Not Listed |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')