Grav Admin Plugin vulnerable to Cross-Site Scripting (XSS) Stored endpoint `/admin/config/site` parameter `data[taxonomies]`
Description
This admin plugin for Grav is an HTML user interface that provides a convenient way to configure Grav and easily create and modify pages. Prior to 1.11.0-beta.1, a Stored Cross-Site Scripting (XSS) vulnerability was identified in the /admin/config/site endpoint of the Grav application. This vulnerability allows attackers to inject malicious scripts into the data[taxonomies] parameter. The injected payload is stored on the server and automatically executed in the browser of any user who accesses the affected site configuration, resulting in a persistent attack vector. This vulnerability is fixed in 1.11.0-beta.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
getgrav/gravPackagist | < 1.8.0-beta.27 | 1.8.0-beta.27 |
Affected products
1Patches
199f653296504Fix security vulnerabilities: user enumeration and XSS issues
6 files changed · +48 −8
classes/plugin/Controllers/Login/LoginController.php+2 −1 modified@@ -471,7 +471,8 @@ public function taskForgot(): ResponseInterface $interval = $config->get('plugins.login.max_pw_resets_interval', 2); - $this->setMessage($this->translate('PLUGIN_LOGIN.FORGOT_CANNOT_RESET_IT_IS_BLOCKED', $to, $interval), 'error'); + // Security: Use generic message to prevent email enumeration (GHSA-q3qx-cp62-f6m7) + $this->setMessage($this->translate('PLUGIN_ADMIN.FORGOT_CANNOT_RESET_RATE_LIMITED', $interval), 'error'); return $this->createRedirectResponse($current); }
languages/en.yaml+1 −0 modified@@ -21,6 +21,7 @@ PLUGIN_ADMIN: FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Cannot reset password for %s, no email address is set" FORGOT_USERNAME_DOES_NOT_EXIST: "User with username <b>%s</b> does not exist" FORGOT_EMAIL_NOT_CONFIGURED: "Cannot reset password. This site is not configured to send emails" + FORGOT_CANNOT_RESET_RATE_LIMITED: "Password reset temporarily blocked due to too many attempts. Please try again later (maximum %s minutes)" FORGOT_EMAIL_SUBJECT: "%s Password Reset Request" FORGOT_EMAIL_BODY: "<h1>Password Reset</h1><p>Dear %1$s,</p><p>A request was made on <b>%4$s</b> to reset your password.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Click this to reset your password</a><br /><br /></p><p>Alternatively, copy the following URL into your browser's address bar:</p> <p>%2$s</p><p><br />Kind regards,<br /><br />%3$s</p>" MANAGE_PAGES: "Manage Pages"
themes/grav/app/forms/fields/selectize.js+17 −0 modified@@ -19,6 +19,17 @@ const PagesRoute = { } }; +// Security: Default render functions that escape HTML to prevent XSS +// (GHSA-65mj-f7p4-wggq, GHSA-7g78-5g5g-mvfj, GHSA-mpjj-4688-3fxg) +const SafeRender = { + option: function(item, escape) { + return `<div>${escape(item.text || item.value)}</div>`; + }, + item: function(item, escape) { + return `<div>${escape(item.text || item.value)}</div>`; + } +}; + export default class SelectizeField { constructor(options = {}) { this.options = Object.assign({}, options); @@ -45,6 +56,12 @@ export default class SelectizeField { data = $.extend({}, data, { render: PagesRoute }); } + // Security: Apply safe render functions by default to escape HTML + // Only apply if no custom render is already defined + if (!data.render) { + data = $.extend({}, data, { render: SafeRender }); + } + if (!field.length || field.get(0).selectize) { return; } const plugins = $.merge(data.plugins ? data.plugins : [], ['required-fix']); field.selectize($.extend({}, data, { plugins }));
themes/grav/js/admin.min.js+24 −5 modified@@ -2434,12 +2434,12 @@ var SafeUpgrade = /*#__PURE__*/function () { var version = data.version || {}; var releaseDate = version.release_date || ''; var packageSize = version.package_size ? formatBytes(version.package_size) : t('SAFE_UPGRADE_UNKNOWN_SIZE', 'unknown'); -var warnings = data.preflight && data.preflight.warnings || []; + var warnings = data.preflight && data.preflight.warnings || []; var pending = data.preflight && data.preflight.plugins_pending || {}; - var isMajorUpgrade = !!(data.preflight && data.preflight.is_major_minor_upgrade); - var hasPendingUpdates = Object.keys(pending).length > 0; var psrConflicts = data.preflight && data.preflight.psr_log_conflicts || {}; var monologConflicts = data.preflight && data.preflight.monolog_conflicts || {}; + var isMajorUpgrade = !!(data.preflight && data.preflight.is_major_minor_upgrade); + var hasPendingUpdates = Object.keys(pending).length > 0; if (data.status === 'error') { blockers.push(data.message || t('SAFE_UPGRADE_GENERIC_ERROR', 'Safe upgrade could not complete. See Grav logs for details.')); } @@ -2486,7 +2486,7 @@ var warnings = data.preflight && data.preflight.warnings || []; var warningsList = filteredWarnings.length || psrWarningItems.length || monologWarningItems.length ? "\n <section class=\"safe-upgrade-panel safe-upgrade-panel--alert safe-upgrade-alert\">\n <header class=\"safe-upgrade-panel__header\">\n <div class=\"safe-upgrade-panel__title-wrap\">\n <span class=\"safe-upgrade-panel__icon fa fa-exclamation-triangle\" aria-hidden=\"true\"></span>\n <div>\n <strong class=\"safe-upgrade-panel__title\">".concat(t('SAFE_UPGRADE_WARNINGS', 'Warnings'), "</strong>\n <span class=\"safe-upgrade-panel__subtitle\">").concat(t('SAFE_UPGRADE_WARNINGS_HINT', 'These items may require attention before continuing.'), "</span>\n </div>\n </div>\n </header>\n <div class=\"safe-upgrade-panel__body\">\n <ul>\n ").concat(filteredWarnings.map(function (warning) { return "<li>".concat(warning, "</li>"); }).join(''), "\n ").concat(psrWarningItems.join(''), "\n ").concat(monologWarningItems.join(''), "\n </ul>\n </div>\n </section>\n ") : ''; -var pendingList = hasPendingUpdates ? "\n <section class=\"safe-upgrade-panel safe-upgrade-panel--info safe-upgrade-pending\">\n <header class=\"safe-upgrade-panel__header\">\n <div class=\"safe-upgrade-panel__title-wrap\">\n <span class=\"safe-upgrade-panel__icon fa fa-sync\" aria-hidden=\"true\"></span>\n <div>\n <strong class=\"safe-upgrade-panel__title\">".concat(t('SAFE_UPGRADE_PENDING_UPDATES', 'Pending plugin or theme updates'), "</strong>\n <span class=\"safe-upgrade-panel__subtitle\">").concat(isMajorUpgrade ? t('SAFE_UPGRADE_PENDING_INTRO', 'Because this is a major Grav upgrade, update these extensions first to ensure maximum compatibility.') : t('SAFE_UPGRADE_PENDING_MINOR_DESC', 'These updates are optional for this release; apply them at your convenience.'), "</span>\n </div>\n </div>\n </header>\n <div class=\"safe-upgrade-panel__body\">\n <ul>\n ").concat(Object.keys(pending).map(function (slug) { + var pendingList = hasPendingUpdates ? "\n <section class=\"safe-upgrade-panel safe-upgrade-panel--info safe-upgrade-pending\">\n <header class=\"safe-upgrade-panel__header\">\n <div class=\"safe-upgrade-panel__title-wrap\">\n <span class=\"safe-upgrade-panel__icon fa fa-sync\" aria-hidden=\"true\"></span>\n <div>\n <strong class=\"safe-upgrade-panel__title\">".concat(t('SAFE_UPGRADE_PENDING_UPDATES', 'Pending plugin or theme updates'), "</strong>\n <span class=\"safe-upgrade-panel__subtitle\">").concat(isMajorUpgrade ? t('SAFE_UPGRADE_PENDING_INTRO', 'Because this is a major Grav upgrade, update these extensions first to ensure maximum compatibility.') : t('SAFE_UPGRADE_PENDING_MINOR_DESC', 'These updates are optional for this release; apply them at your convenience.'), "</span>\n </div>\n </div>\n </header>\n <div class=\"safe-upgrade-panel__body\">\n <ul>\n ").concat(Object.keys(pending).map(function (slug) { var item = pending[slug] || {}; var type = item.type || 'plugin'; var current = item.current || t('SAFE_UPGRADE_UNKNOWN_VERSION', 'unknown'); @@ -6091,6 +6091,17 @@ var PagesRoute = { return "<div class=\"selectize-route-option\">\n <span class=\"text-grey\">".concat(arrows, "</span>\n <span>\n <span class=\"text-update\">").concat(slug.replace('(', '/').replace(')', ''), "</span>\n <span>").concat(label.join(' '), "</span>\n </span>\n </div>"); } }; + +// Security: Default render functions that escape HTML to prevent XSS +// (GHSA-65mj-f7p4-wggq, GHSA-7g78-5g5g-mvfj, GHSA-mpjj-4688-3fxg) +var SafeRender = { + option: function option(item, escape) { + return "<div>".concat(escape(item.text || item.value), "</div>"); + }, + item: function item(_item, escape) { + return "<div>".concat(escape(_item.text || _item.value), "</div>"); + } +}; var SelectizeField = /*#__PURE__*/function () { function SelectizeField() { var _this = this; @@ -6119,6 +6130,14 @@ var SelectizeField = /*#__PURE__*/function () { render: PagesRoute }); } + + // Security: Apply safe render functions by default to escape HTML + // Only apply if no custom render is already defined + if (!data.render) { + data = external_jQuery_default().extend({}, data, { + render: SafeRender + }); + } if (!field.length || field.get(0).selectize) { return; } @@ -15071,4 +15090,4 @@ module.exports = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADz /******/ Grav = __webpack_exports__; /******/ /******/ })() -; +; \ No newline at end of file
themes/grav/templates/forms/fields/acl_picker/acl_picker.html.twig+2 −1 modified@@ -36,7 +36,8 @@ {% set optionsList = [] %} {% endif %} {% for group in groups.index %} - {% set optionsList = optionsList|merge([{ text: group.readableName ?? group.groupname, value: group.groupname }]) %} + {# Security: Escape HTML in group names to prevent XSS (GHSA-rmw5-f87r-w988) #} + {% set optionsList = optionsList|merge([{ text: (group.readableName ?? group.groupname)|e, value: group.groupname }]) %} {% endfor %} {% endif %}
themes/grav/templates/forms/fields/taxonomy/taxonomy.html.twig+2 −1 modified@@ -21,10 +21,11 @@ {% endif %} {% set list = (options[name] ?? [])|merge(sub_taxonomies)|merge(value)|array_unique %} + {# Security: Escape HTML in taxonomy names and values to prevent XSS (GHSA-gqxx-248x-g29f, GHSA-mpjj-4688-3fxg) #} {% set field = { type: 'select', classes: 'fancy create', - label: name|capitalize, + label: name|capitalize|e, name: field_name, multiple: true, options: list,
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-gqxx-248x-g29fghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-66308ghsaADVISORY
- github.com/getgrav/grav-plugin-admin/commit/99f653296504f1d6408510dd2f6f20a45a26f9b0ghsax_refsource_MISCWEB
- github.com/getgrav/grav/security/advisories/GHSA-gqxx-248x-g29fghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.