Skip to content

Administration: Restrict find-posts results to posts the current user can edit - #15256

Open
erycson wants to merge 2 commits into
WordPress:trunkfrom
erycson:fix/find-posts-private-disclosure
Open

erycson wants to merge 2 commits into
WordPress:trunkfrom
erycson:fix/find-posts-private-disclosure

Conversation

@erycson

@erycson erycson commented Oct 30, 2026

Copy link
Copy Markdown

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

Problem

wp_ajax_find_posts() (the "Find Posts" / "Attach to existing content" modal)
queries with 'post_status' => 'any' and no 'perm':

$args = array(
	'post_type'      => array_keys( $post_types ),
	'post_status'    => 'any',
	'posts_per_page' => 50,
);

The only gate on the handler is check_ajax_referer( 'find-posts' ), and that
nonce is available to anyone who can open the media library (upload_files,
i.e. Author and up). As a result, a low-privileged user can read the titles
of other users' private and draft posts
by searching in this modal.

The 'perm' author restriction in WP_Query is never applied when the status
is 'any' (it is only evaluated in the per-status branch), which is the
behavior johnbillion already noted in comment:6 of #40340.

Fix

Pass 'perm' => 'editable' and an explicit status list so the query engages
the author restriction and only returns posts the current user may actually
edit — which is what the modal is for (attaching media to an editable post):

$args = array(
	'post_type'      => array_keys( $post_types ),
	'post_status'    => array( 'publish', 'future', 'draft', 'pending', 'private' ),
	'perm'           => 'editable',
	'posts_per_page' => 50,
);

Users who can edit others' posts (Editor, Administrator) are unaffected and
still see all matching posts.

Tests

Adds tests/phpunit/tests/ajax/wpAjaxFindPosts.php covering:

  • an Author does not see another user's private post;
  • an Author does not see another user's draft;
  • the Author's own posts are still listed (the modal keeps working);
  • an Editor still sees private posts.

Verified locally: the two disclosure tests fail on trunk and pass with this
change. phpcs is clean on both files.

Note for reviewers

'perm' => 'editable' narrows the modal to editable posts. This matches its
purpose, but it is a behavior change worth confirming: custom post statuses
registered by plugins are no longer included (the previous 'any' included any
status not marked exclude_from_search). Happy to adjust the status list if
preferred.

See #40340.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code, with code-intruder (my own static analysis tool)
Model(s): Claude Opus, Claude Haiku
Used for: code-intruder flagged the handler as reachable without a capability
check (only a nonce guards it) and an AI review triaged the finding. AI helped
reproduce it on a local multisite install (as an Author, searching returned
another user's private/draft post title) and write the test. I reviewed and
tested the change myself.

wp_ajax_find_posts() queried with 'post_status' => 'any' and no 'perm',
so any user able to open the media library ('upload_files') could read the
titles of other users' private and draft posts through the "Find Posts"
modal. With 'any', WP_Query skips the 'perm' author restriction entirely.

Pass 'perm' => 'editable' and an explicit status list so the query is
limited to posts the current user may edit, which is what the modal is for
(attaching media to an editable post). Users who can edit others' posts are
unaffected.

Add ajax tests covering the private and draft leak, that the user's own
posts are still listed, and that an editor still sees private posts.

See #40340.
@github-actions

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.

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

Props extremsx.

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


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@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

  • 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.

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.

1 participant