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

Grav Admin Plugin vulnerable to User Enumeration & Email Disclosure

CVE-2025-66307

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 user enumeration and email disclosure vulnerability exists in Grav. The "Forgot Password" functionality at /admin/forgot leaks information about valid usernames and their associated email addresses through distinct server responses. This allows an attacker to enumerate users and disclose sensitive email addresses, which can be leveraged for targeted attacks such as password spraying, phishing, or social engineering. 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

5

News mentions

0

No linked articles in our index yet.