Skip to content

Fix: Misleading Message When Accessing Admin Menu Page - #7613

Closed
nirajgiriXD wants to merge 47 commits into
WordPress:trunkfrom
nirajgiriXD:fix/misleading-permissions-message
Closed

nirajgiriXD wants to merge 47 commits into
WordPress:trunkfrom
nirajgiriXD:fix/misleading-permissions-message

Conversation

@nirajgiriXD

Copy link
Copy Markdown

Description

This PR addresses the issue where the misleading message You do not have sufficient permissions to access this page. was displayed when attempting to access an admin page that doesn't exist. Instead of suggesting a permissions problem, the fix ensures that users are now shown a clearer message: The requested page does not exist. This provides a more accurate and user-friendly explanation when the page is missing or unavailable.

Screenshot

the-requested-page-does-not-exists

Testing Instructions

  • Install a plugin that includes a settings page.
  • Navigate to the plugin's settings page and copy the URL.
  • Go to the plugins page and deactivate the plugin you just installed.
  • Paste the copied URL of the plugin's settings page into the browser's address bar and press Enter.
  • You should now see the message The requested page does not exist. instead of You do not have sufficient permissions to access this page.

Trac ticket: https://core.trac.wordpress.org/ticket/14060

@github-actions

github-actions Bot commented Oct 22, 2024 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @nirajgiri9860@googlemail.com.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

Core Committers: Use this line as a base for the props when committing in SVN:

Props nirajgirixd, wildworks, westonruter, peterwilsoncc, apermo, mindctrl, noruzzaman, shailu25, huzaifaalmesbah, muryam.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Comment thread src/wp-admin/includes/plugin.php
Co-authored-by: Christoph Daum <c.daum@me.com>
@nirajgiriXD
nirajgiriXD requested a review from apermo October 25, 2024 12:22

@apermo apermo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me

Comment thread src/wp-admin/includes/menu.php Outdated
Comment thread src/wp-admin/includes/menu.php Outdated
Comment thread src/wp-admin/includes/plugin.php Outdated
@mindctrl

Copy link
Copy Markdown
Member

@nirajgiriXD I left some feedback for consideration. Also, could you bring this up to date with the latest trunk branch?

nirajgiriXD and others added 3 commits July 24, 2025 09:08
Co-authored-by: John Parris <public@johnparris.com>
Co-authored-by: John Parris <public@johnparris.com>
@nirajgiriXD

Copy link
Copy Markdown
Author

@mindctrl Thanks for the feedback! I've implemented the suggested changes and updated the branch with the latest from trunk.

@mindctrl mindctrl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs unit tests. Needs discussion about introducing a new action, if it's desired, and what its name should be.

Comment thread src/wp-admin/includes/menu.php Outdated
Comment thread src/wp-admin/includes/plugin.php Outdated
Comment thread src/wp-admin/includes/plugin.php Outdated
Comment thread src/wp-admin/includes/plugin.php Outdated
Comment thread src/wp-admin/includes/plugin.php
Comment thread src/wp-admin/includes/plugin.php Outdated
nirajgiriXD and others added 6 commits December 2, 2025 09:04
Co-authored-by: Noruzzaman <noruzzamanrubel@gmail.com>
Co-authored-by: Noruzzaman <noruzzamanrubel@gmail.com>
Co-authored-by: Noruzzaman <noruzzamanrubel@gmail.com>
Co-authored-by: Noruzzaman <noruzzamanrubel@gmail.com>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Co-authored-by: Noruzzaman <noruzzamanrubel@gmail.com>
Comment thread src/wp-admin/includes/menu.php Outdated
Comment thread src/wp-admin/includes/plugin.php Outdated
Comment thread src/wp-admin/includes/plugin.php Outdated
Comment thread src/wp-admin/includes/plugin.php Outdated
nirajgiriXD and others added 2 commits February 17, 2026 08:48
Co-authored-by: Shail Mehta <shailmehta25@gmail.com>
Co-authored-by: Huzaifa Al Mesbah <huzaifah4m@gmail.com>
westonruter and others added 4 commits September 21, 2026 11:43
`$_wp_menu_nopriv` and `$_wp_submenu_nopriv` were documented as bare `array`,
which leaves PHPStan resolving their contents as `mixed` and reporting both a
missing iterable value type and an inaccessible offset on every nested lookup.

Both shapes are determinable from core. `$_wp_menu_nopriv` is written only in
`wp-admin/includes/menu.php`, keyed by top-level menu slug with a value of
`true`. `$_wp_submenu_nopriv` is written there and in `add_submenu_page()`,
keyed by parent slug then by submenu slug, again with a value of `true`. Every
read is an `isset()`, so only key presence is ever consumed.

Documents them as `array<string, true>` and
`array<string, array<string, true>>` respectively, on the two functions this
branch modifies. Docblocks only; no behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven of the eight `@global` tags on the function carried a type and a name but
no description. Fills them in, reusing the wording already used for the same
globals on `wp_admin_page_exists()` so the two functions read consistently.

`$menu` and `$submenu` are described for the first time; neither is documented
anywhere else in core.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@westonruter

Copy link
Copy Markdown
Member

🤖 Comment by Claude Opus 5

This is a review of the branch diff. The structural claims below were each verified against the current branch. Findings are ordered by severity.

The no-privilege registries are emptied after _admin_menu fires

src/wp-admin/includes/menu.php fires _admin_menu on line 41, then resets both registries on lines 84–85:

$_wp_submenu_nopriv = array();
$_wp_menu_nopriv    = array();

An add_submenu_page() call made before that point — on _admin_menu, or earlier still on init or plugins_loaded — takes the early return at src/wp-admin/includes/plugin.php:1505, which records $_wp_submenu_nopriv[ $parent_slug ][ $menu_slug ] and deliberately touches neither $submenu nor $_registered_pages. That record is then erased before wp_admin_page_exists() ever consults it.

The consequence is that a low-privilege user requesting such a page is now told "The requested page does not exist.", when previously a permission error was shown. This is the same class of misleading message the PR sets out to fix, inverted. Registrations on admin_menu (line 168) are unaffected, since that hook fires after the reset, so the exposure is limited to the early window — but that window is where a number of plugins do register.

admin_page_access_denied no longer fires for unregistered slugs

With the new elseif at src/wp-admin/includes/menu.php:375, a request for a page that does not exist reaches wp_die() on the wp_admin_page_exists() branch and never reaches do_action( 'admin_page_access_denied' ) on line 392.

That hook is used by plugins to intercept stale or mistyped ?page= requests and redirect them — to a dashboard, an onboarding screen, or a renamed page. Those redirects will silently stop working for exactly the requests they were most often installed to catch. Whether or not the new behavior is preferred, it is a back-compat break that warrants a mention in the commit message and a dev note.

$admin_page_parent becomes a per-request memo on a public function

At src/wp-admin/includes/plugin.php:2237:

$parent = $admin_page_parent ?? get_admin_page_parent();

user_can_access_admin_page() is a public function that plugins call directly, and menu.php now populates $admin_page_parent early by way of wp_admin_page_exists(). Every later call therefore reuses the parent computed for the original request. A plugin that temporarily assigns $GLOBALS['plugin_page'] and then asks about access — or that re-parents menus through remove_menu_page() — receives an answer derived from the wrong parent, and so a wrong $hookname and a wrong boolean.

The global name is also generic enough that collision with a plugin's own global is plausible, and ?? would then silently honor the poisoned value rather than recomputing. Passing the parent as an optional argument, or keying the memo on $plugin_page/$pagenow, would avoid both problems.

The existence check ahead of the capability check creates an enumeration oracle

Because wp_admin_page_exists() is consulted first, any authenticated user who can reach admin.php can now distinguish "does not exist" from "not allowed" for an arbitrary ?page= slug. That reveals which plugins are installed and which admin page slugs they register. Previously both cases produced an identical permission error.

This is inherent to the feature rather than a defect in the implementation, and the disclosure is modest. It is raised only so that it is an explicit decision on the ticket rather than an incidental one.

Test observations

tests/phpunit/tests/admin/includesPlugin.php:111 uses global $plugin_page; unset( $plugin_page );, which unsets only the local reference — $GLOBALS['plugin_page'] survives, and wp_admin_page_exists() re-imports it through its own global statement. The assertion passes only because reset_menu_globals() already unset the global correctly during set-up, so the test does not exercise the condition it is named for. unset( $GLOBALS['plugin_page'] ); would.

test_wp_admin_page_exists_returns_true_for_a_registered_page() builds $_registered_pages on line 125 from the same get_plugin_page_hookname( $plugin_page, $admin_page_parent ) call that the function under test performs, so it asserts that an expression equals itself and would still pass if hookname derivation were wrong. Registering through the public API as an administrator — as the fourth test already does — would give it teeth.

Noted and discounted

The scan at src/wp-admin/includes/plugin.php:2175 iterates every parent in $_wp_submenu_nopriv rather than scoping to the resolved parent, so tools.php?page=foo reports as existing when foo is a no-privilege submenu of options-general.php. That errs toward a permission error instead of a 404, which is the safer direction here, and appears deliberate.

@t-hamano t-hamano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on this feedback, I have proposed specific changes.

admin_page_access_denied no longer fires for unregistered slugs

I think this is worth discussing how to address. Since we now have the new admin_page_not_found action, we could probably cover this change with a dev note. Something like:

In WordPress 7.2, requests for admin pages that don't exist now show "The requested page does not exist." (404) instead of a permission error (403). As a result, admin_page_access_denied no longer fires for such requests; it only fires when the page exists but the user lacks permission. Plugins that use admin_page_access_denied to redirect old or mistyped ?page= URLs should also hook their callback to the new admin_page_not_found action. Hooking to both is safe on older WordPress versions, since admin_page_not_found simply won't fire there.

Comment thread src/wp-admin/includes/plugin.php
Comment thread src/wp-admin/includes/plugin.php Outdated
Comment thread tests/phpunit/tests/admin/includesPlugin.php Outdated
Comment thread tests/phpunit/tests/admin/includesPlugin.php Outdated
@westonruter

Copy link
Copy Markdown
Member

@nirajgiriXD You can skip merging from trunk unless there are merge conflicts. Otherwise, this most likely this just waste CPU time running the jobs and it creates noise.

t-hamano and others added 4 commits October 7, 2026 18:38
The no-privilege menu registries are reset in menu.php after `_admin_menu`
fires, so pages registered earlier were reported as missing.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Parent-agnostic slug checks can misclassify nonexistent submenu URLs as permission-denied pages.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)

Comment thread src/wp-admin/includes/plugin.php

@peterwilsoncc peterwilsoncc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some notes inline.

🔢 if the test suite indicates that the note applies to multiple tests.

Comment thread src/wp-admin/includes/menu.php Outdated
Comment thread tests/phpunit/tests/admin/includesPlugin.php Outdated
Comment thread tests/phpunit/tests/admin/includesPlugin.php
t-hamano and others added 3 commits October 12, 2026 16:31
Co-Authored-By: Claude <noreply@anthropic.com>
The test suite resets the current user during tear down.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>

@t-hamano t-hamano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good from my end. The only notable point is the following, though I personally think this can be addressed in the dev notes.

admin_page_access_denied no longer fires for unregistered slugs

I think this is worth discussing how to address. Since we now have the new admin_page_not_found action, we could probably cover this change with a dev note. Something like:

In WordPress 7.2, requests for admin pages that don't exist now show "The requested page does not exist." (404) instead of a permission error (403). As a result, admin_page_access_denied no longer fires for such requests; it only fires when the page exists but the user lacks permission. Plugins that use admin_page_access_denied to redirect old or mistyped ?page= URLs should also hook their callback to the new admin_page_not_found action. Hooking to both is safe on older WordPress versions, since admin_page_not_found simply won't fire there.

@westonruter westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor typing improvement suggestions.

Comment thread src/wp-admin/includes/plugin.php Outdated
Comment thread src/wp-admin/includes/plugin.php Outdated
* @global array $menu
* @global array $_wp_real_parent_file
* @global bool $_wp_submenu_nopriv
* @global array $_wp_nopriv_pages

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: This here is still just array but for wp_admin_page_exists() it is richly typed as array<string, true>. It would be nice to include the richer typing here as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 369fb0d

Comment thread src/wp-admin/includes/plugin.php Outdated
*
* @since 7.2.0
*
* @global string $plugin_page The plugin page slug being loaded.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this could be null, right? The first statement in the function is to check if it is set.

Suggested change
* @global string $plugin_page The plugin page slug being loaded.
* @global string|null $plugin_page The plugin page slug being loaded.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 369fb0d

Comment thread src/wp-admin/includes/plugin.php Outdated
Comment on lines +2226 to +2232
* @global string $pagenow The filename of the current screen.
* @global array $menu The top-level admin menu items.
* @global array $submenu The admin submenu items, keyed by parent slug.
* @global array<string, true> $_wp_menu_nopriv Array of top-level menu slugs the current user cannot access.
* @global array<string, array<string, true>> $_wp_submenu_nopriv Array of submenu slugs the current user cannot access, keyed by parent slug.
* @global string $plugin_page The plugin page slug being loaded.
* @global array $_registered_pages Array of all registered admin page hooks.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can be more richly typed to match the above as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 369fb0d

@peterwilsoncc peterwilsoncc added the props-bot Adding this label triggers the Props Bot workflow for a PR. label Oct 15, 2026
@github-actions github-actions Bot removed the props-bot Adding this label triggers the Props Bot workflow for a PR. label Oct 15, 2026
@github-actions

Copy link
Copy Markdown

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 64351
GitHub commit: 6dc9323

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions Bot closed this Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.