Concrete CMS version 9 prior to 9.2.8 and previous versions prior to 8.5.16 are vulnerable to Stored XSS in the Search Field.
Description
Concrete CMS version 9 prior to 9.2.8 and previous versions prior to 8.5.16 are vulnerable to Stored XSS in the Search Field. Prior to the fix, stored XSS could be executed by an administrator changing a filter to which a rogue administrator had previously added malicious code. The Concrete CMS security team gave this vulnerability a CVSS v3.1 score of 3.1 with a vector of AV:N/AC:H/PR:H/UI:R/S:U/C:N/I:L/A:L https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator . Thanks Alexey Solovyev for reporting
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Concrete CMS 9 prior to 9.2.8 and 8.5.16 are vulnerable to stored XSS in the Search Field, allowing a rogue administrator to inject malicious code that executes when another admin modifies the filter.
Vulnerability
Overview A stored cross-site scripting (XSS) vulnerability exists in the Search Field of Concrete CMS versions 9 prior to 9.2.8 and versions prior to 8.5.16. The root cause is insufficient sanitization of user-supplied input when saving search filter preset names and file download link text [1][2][3]. An authenticated administrator with the ability to create or modify search filters can inject arbitrary JavaScript code into the preset name or link text fields [1].
Exploitation
To exploit this vulnerability, an attacker must have administrative privileges on a Concrete CMS instance. The attacker creates or modifies a search filter, embedding malicious script in the preset name or link text. Later, when another administrator edits or changes that filter, the injected script executes in the context of the victim's browser session [1]. The CVSS vector (AV:N/AC:H/PR:H/UI:R/S:U/C:N/I:L/A:L) indicates the attack requires high complexity and privileges, and relies on user interaction from another admin [1].
Impact
A successful stored XSS attack allows the rogue administrator to perform actions on behalf of the victim administrator within the CMS, potentially altering configurations or exfiltrating session tokens. The impact is limited to low integrity and availability impacts, as the vulnerable field is only accessible to administrators and the attack requires a high level of privileges [1].
Mitigation
Concrete CMS addressed this vulnerability in version 9.2.8 (commit 822e689) and version 8.5.16 (commit e85ef24) by applying output encoding via the h() function to user-supplied data in the affected templates [2][3][4]. All users should upgrade to the latest patched version to prevent exploitation.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
concrete5/concrete5Packagist | >= 9.0.0RC1, < 9.2.8 | 9.2.8 |
concrete5/concrete5Packagist | < 8.5.16 | 8.5.16 |
Affected products
2- Concrete CMS/Concrete CMSv5Range: 9.0.0
Patches
417020e35940c54cb334ddefce85ef2408a5eMerge pull request #12007 from KorvinSzanto/8.5.x-output-cleanup
9 files changed · +20 −14
concrete/blocks/file/view.php+1 −1 modified@@ -10,7 +10,7 @@ ?> <div class="ccm-block-file"> <a href="<?php echo $forceDownload ? $f->getForceDownloadURL() : $f->getDownloadURL(); - ?>"><?php echo stripslashes($controller->getLinkText()) ?></a> + ?>"><?php echo h(stripslashes($controller->getLinkText())) ?></a> </div>
concrete/controllers/dialog/express/preset/delete.php+1 −1 modified@@ -65,7 +65,7 @@ public function remove_search_preset() } if (!$this->error->has()) { $response = new EditResponse(); - $response->setMessage(t('%s deleted successfully.', $searchPreset->getPresetName())); + $response->setMessage(t('%s deleted successfully.', h($searchPreset->getPresetName()))); $response->setAdditionalDataAttribute('presetID', $presetID); $em = $this->app->make(\Doctrine\ORM\EntityManager::class); $em->remove($searchPreset);
concrete/controllers/dialog/file/preset/delete.php+1 −1 modified@@ -46,7 +46,7 @@ public function remove_search_preset() } if (!$this->error->has()) { $response = new EditResponse(); - $response->setMessage(t('%s deleted successfully.', $searchPreset->getPresetName())); + $response->setMessage(t('%s deleted successfully.', h($searchPreset->getPresetName()))); $response->setAdditionalDataAttribute('presetID', $presetID); $node = TreeNodeSearchPreset::getNodeBySavedSearchID($presetID); if (is_object($node)) {
concrete/controllers/dialog/search/preset/delete.php+1 −1 modified@@ -43,7 +43,7 @@ public function remove_search_preset() } if (!$this->error->has()) { $response = new EditResponse(); - $response->setMessage(t('%s deleted successfully.', $searchPreset->getPresetName())); + $response->setMessage(t('%s deleted successfully.', h($searchPreset->getPresetName()))); $response->setAdditionalDataAttribute('presetID', $presetID); $em = $this->app->make(EntityManager::class); $em->remove($searchPreset);
concrete/controllers/dialog/search/preset/edit.php+1 −1 modified@@ -44,7 +44,7 @@ public function edit_search_preset() } if (!$this->error->has()) { $response = new EditResponse(); - $response->setMessage(t('%s edited successfully.', $newPresetName)); + $response->setMessage(t('%s edited successfully.', h($newPresetName))); $response->setAdditionalDataAttribute('presetID', $presetID); $response->setAdditionalDataAttribute('actionURL', (string) $this->getSavedSearchBaseURL($searchPreset)); $searchPreset->setPresetName($newPresetName);
concrete/single_pages/dashboard/system/calendar/colors.php+5 −5 modified@@ -11,14 +11,14 @@ <div class="form-inline"> <?=$form->label('defaultBackgroundColor', t('Background'))?> - <?=$color->output('defaultBackgroundColor', $defaultBackgroundColor)?> + <?=$color->output('defaultBackgroundColor', h($defaultBackgroundColor))?> </div> </div> <div class="form-group col-sm-5"> <div class="form-inline"> <?=$form->label('defaultTextColor', t('Text'))?> - <?=$color->output('defaultTextColor', $defaultTextColor)?> + <?=$color->output('defaultTextColor', h($defaultTextColor))?> </div> </div> </div> @@ -53,14 +53,14 @@ <tr> <td style="text-align: center; width: 10px"><?=$form->checkbox('override[]', $topic->getTreeNodeID(), $checked)?></td> <td style="width: 50%"><?=$topic->getTreeNodeDisplayName()?></td> - <td><?=$color->output('backgroundColor[' . $topic->getTreeNodeID() . ']', $backgroundColor)?></td> - <td><?=$color->output('textColor[' . $topic->getTreeNodeID() . ']', $textColor)?></td> + <td><?=$color->output('backgroundColor[' . $topic->getTreeNodeID() . ']', h($backgroundColor))?></td> + <td><?=$color->output('textColor[' . $topic->getTreeNodeID() . ']', h($textColor))?></td> </tr> <?php } ?> </table> - <?php + <?php } else { ?> <p><?=t('You have not defined any categories for your calendars.')?></p>
concrete/src/StyleCustomizer/Inline/StyleSet.php+8 −2 modified@@ -247,8 +247,14 @@ public static function populateFromRequest(Request $request) $v = $post->get('customClass'); if (is_array($v)) { - $set->setCustomClass(implode(' ', $v)); - $return = true; + $v = array_filter($v, function ($class) { + return preg_match('/^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/', $class); + }); + + if (count($v) > 0) { + $set->setCustomClass(implode(' ', $v)); + $return = true; + } } $v = trim($post->get('customID', ''));
concrete/views/dialogs/search/preset/delete.php+1 −1 modified@@ -5,7 +5,7 @@ <?= $token->output('remove_search_preset'); ?> <?= $form->hidden('presetID', $searchPreset->getId()); ?> <?= $form->hidden('objectID', $controller->getObjectID()); ?> - <p><?= t('Are you sure you want to remove the "%s" search preset?', $searchPreset->getPresetName()); ?></p> + <p><?= t('Are you sure you want to remove the "%s" search preset?', h($searchPreset->getPresetName())); ?></p> <div class="dialog-buttons"> <button class="btn btn-default" data-dialog-action="cancel"><?= t('Cancel'); ?></button>
concrete/views/dialogs/search/preset/edit.php+1 −1 modified@@ -7,7 +7,7 @@ <?= $form->hidden('objectID', $controller->getObjectID()); ?> <div class="form-group"> <?= $form->label('presetName', t('Name')); ?> - <?= $form->text('presetName', $searchPreset->getPresetName()); ?> + <?= $form->text('presetName', h($searchPreset->getPresetName())); ?> </div> <div class="dialog-buttons">
822e689cefe1Output cleanup (#11988)
9 files changed · +20 −14
concrete/blocks/file/view.php+1 −1 modified@@ -16,7 +16,7 @@ ?> <div class="ccm-block-file"> <a href="<?php echo (!empty($forceDownload)) ? $f->getForceDownloadURL() : $f->getDownloadURL(); ?>"> - <?php echo stripslashes($controller->getLinkText()) ?> + <?php echo h(stripslashes($controller->getLinkText())) ?> </a> </div> <?php
concrete/controllers/dialog/express/preset/delete.php+1 −1 modified@@ -71,7 +71,7 @@ public function remove_search_preset() } if (!$this->error->has()) { $response = new EditResponse(); - $response->setMessage(t('%s deleted successfully.', $searchPreset->getPresetName())); + $response->setMessage(t('%s deleted successfully.', h($searchPreset->getPresetName()))); $response->setAdditionalDataAttribute('presetID', $presetID); $em = $this->app->make(\Doctrine\ORM\EntityManager::class); $em->remove($searchPreset);
concrete/controllers/dialog/file/preset/delete.php+1 −1 modified@@ -46,7 +46,7 @@ public function remove_search_preset() } if (!$this->error->has()) { $response = new EditResponse(); - $response->setMessage(t('%s deleted successfully.', $searchPreset->getPresetName())); + $response->setMessage(t('%s deleted successfully.', h($searchPreset->getPresetName()))); $response->setAdditionalDataAttribute('presetID', $presetID); $node = TreeNodeSearchPreset::getNodeBySavedSearchID($presetID); if (is_object($node)) {
concrete/controllers/dialog/search/preset/delete.php+1 −1 modified@@ -48,7 +48,7 @@ public function remove_search_preset() } if (!$this->error->has()) { $response = new EditResponse(); - $response->setMessage(t('%s deleted successfully.', $searchPreset->getPresetName())); + $response->setMessage(t('%s deleted successfully.', h($searchPreset->getPresetName()))); $response->setAdditionalDataAttribute('presetID', $presetID); $em = $this->app->make(EntityManager::class); $em->remove($searchPreset);
concrete/controllers/dialog/search/preset/edit.php+1 −1 modified@@ -49,7 +49,7 @@ public function edit_search_preset() } if (!$this->error->has()) { $response = new EditResponse(); - $response->setMessage(t('%s edited successfully.', $newPresetName)); + $response->setMessage(t('%s edited successfully.', h($newPresetName))); $response->setAdditionalDataAttribute('presetID', $presetID); $response->setAdditionalDataAttribute('actionURL', (string) $this->getSavedSearchBaseURL($searchPreset)); $searchPreset->setPresetName($newPresetName);
concrete/single_pages/dashboard/system/calendar/colors.php+5 −5 modified@@ -8,11 +8,11 @@ <legend><?=t('Default Colors')?></legend> <div class="form-group"> <?=$form->label('defaultBackgroundColor', t('Background'))?> - <?=$color->output('defaultBackgroundColor', $defaultBackgroundColor)?> + <?=$color->output('defaultBackgroundColor', h($defaultBackgroundColor))?> </div> <div class="form-group"> <?=$form->label('defaultTextColor', t('Text'))?> - <?=$color->output('defaultTextColor', $defaultTextColor)?> + <?=$color->output('defaultTextColor', h($defaultTextColor))?> </div> </fieldset> @@ -45,10 +45,10 @@ <tr> <td style="text-align: center; width: 10px"><?=$form->checkbox('override[]', $topic->getTreeNodeID(), $checked)?></td> <td style="width: 50%"><?=$topic->getTreeNodeDisplayName()?></td> - <td><?=$color->output('backgroundColor[' . $topic->getTreeNodeID() . ']', $backgroundColor)?></td> - <td><?=$color->output('textColor[' . $topic->getTreeNodeID() . ']', $textColor)?></td> + <td><?=$color->output('backgroundColor[' . $topic->getTreeNodeID() . ']', h($backgroundColor))?></td> + <td><?=$color->output('textColor[' . $topic->getTreeNodeID() . ']', h($textColor))?></td> </tr> - <?php + <?php } ?> </table>
concrete/src/StyleCustomizer/Inline/StyleSet.php+8 −2 modified@@ -256,8 +256,14 @@ public static function populateFromRequest(Request $request) $v = $post->get('customClass'); if (is_array($v)) { - $set->setCustomClass(implode(' ', $v)); - $return = true; + $v = array_filter($v, function ($class) { + return preg_match('/^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/', $class); + }); + + if (count($v) > 0) { + $set->setCustomClass(implode(' ', $v)); + $return = true; + } } $v = trim($post->get('customID', ''));
concrete/views/dialogs/search/preset/delete.php+1 −1 modified@@ -4,7 +4,7 @@ <form method="post" data-dialog-form="remove-search-preset" class="form-horizontal" action="<?= $controller->getDeleteSearchPresetAction(); ?>"> <?= $token->output('remove_search_preset'); ?> <?= $form->hidden('presetID', $searchPreset->getId()); ?> - <p><?= t('Are you sure you want to remove the "%s" search preset?', $searchPreset->getPresetName()); ?></p> + <p><?= t('Are you sure you want to remove the "%s" search preset?', h($searchPreset->getPresetName())); ?></p> <div class="dialog-buttons clearfix"> <button class="btn btn-secondary" data-dialog-action="cancel"><?= t('Cancel'); ?></button>
concrete/views/dialogs/search/preset/edit.php+1 −1 modified@@ -6,7 +6,7 @@ <?= $form->hidden('presetID', $searchPreset->getId()); ?> <div class="form-group"> <?= $form->label('presetName', t('Name')); ?> - <?= $form->text('presetName', $searchPreset->getPresetName()); ?> + <?= $form->text('presetName', h($searchPreset->getPresetName())); ?> </div> <div class="dialog-buttons clearfix">
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-qgm9-rxmq-jxmqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-3181ghsaADVISORY
- documentation.concretecms.org/9-x/developers/introduction/version-history/928-release-notesghsaWEB
- documentation.concretecms.org/developers/introduction/version-history/8516-release-notesghsaWEB
- github.com/concretecms/concretecms/commit/822e689cefe1eb876e9de31dad9ce660f3b5c295ghsaWEB
- github.com/concretecms/concretecms/commit/e85ef2408a5eea7d5646178fbef0ab243baaed8fghsaWEB
News mentions
0No linked articles in our index yet.