VYPR
Moderate severityNVD Advisory· Published Dec 1, 2025· Updated Dec 2, 2025

Grav vulnerable to Cross-Site Scripting (XSS) Reflected endpoint /admin/pages/[page], parameter data[header][content][items], located in the "Blog Config" tab

CVE-2025-66309

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 Reflected Cross-Site Scripting (XSS) vulnerability was identified in the /admin/pages/[page] endpoint of the Grav application. This vulnerability allows attackers to inject malicious scripts into the data[header][content][items] parameter. This vulnerability is fixed in 1.11.0-beta.1.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
getgrav/gravPackagist
< 1.8.0-beta.271.8.0-beta.27

Affected products

1

Patches

1
99f653296504

Fix security vulnerabilities: user enumeration and XSS issues

https://github.com/getgrav/grav-plugin-adminAndy MillerNov 30, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.