fix: honor public post statuses and read_private_posts cap in post access checks#3966
Merged
Conversation
…cess checks Post access control over-restricted two cases that WordPress itself exposes on the front-end and via the REST single-post endpoint: 1. Posts in a custom status registered with `public => true` were hidden from anonymous users. The connection status gate (sanitize_post_stati) let only `publish` and `private` through and fell every other status through to an `edit_posts` check, and the Model privacy gate (is_post_private) only treated `publish` as public, so a public custom status was marked private at both the connection and single-node level. (#2819) 2. Users who can `read_private_posts` but not `edit_posts` could not see `private` posts. The connection gate never returned the `private` status for an authorized reader (it only ever denied), and the Model gate applied a blanket `edit_posts` check before considering `read_private_posts`. (#2859) Both gates now share the same rules: `publish` and any status whose `public` flag is true (on a public/publicly_queryable post type) are visible to everyone; `private` requires `read_private_posts`; all other statuses require `edit_posts`. Capability checks use the post type's capability object so custom post type caps (read_private_{cpt}s, etc.) are respected. The revision and auto-draft access logic is unchanged. Adds a `graphql_allowed_post_stati` filter so the queryable statuses for a connection can be adjusted by extensions. Regression tests cover: a public custom status visible to anonymous users in both a connection and as a single node; a read_private_posts user (without edit_posts) seeing private posts while a plain subscriber still cannot; and a non-public custom status staying hidden from anonymous users (locking in the correct behavior from #3248). Closes #2819 Closes #2859
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This was referenced Jun 19, 2026
Post statuses that have been allowed to be viewable by anonymous users not returned in GraphQL
#2819
Closed
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3966 +/- ##
=======================================
Coverage 83.6% 83.6%
- Complexity 5337 5344 +7
=======================================
Files 286 286
Lines 22901 22916 +15
=======================================
+ Hits 19151 19166 +15
Misses 3750 3750
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes two related over-restriction bugs in post access control. Both are scoped as security fixes, and both are cases where WPGraphQL hid content that WordPress itself exposes on the front-end and via the REST single-post endpoint. To be clear, these are over-restrictions, not leaks.
They are fixed together because they are the same underlying logic in two gates (the connection status gate and the Model privacy gate), and the regression test for each must not regress the other.
#2819 — public custom post statuses were hidden
A post in a custom status registered with
public => true(the kind of status WordPress shows on the front-end) was hidden from anonymous users, both in connections and as a single node.PostObjectConnectionResolver::sanitize_post_stati) only letpublishandprivatethrough and fell every other status through to anedit_postscheck, so a public custom status was stripped for anonymous and non-editor users.Post::is_post_private) only treatedpublishas public, so any other status (including a public custom status) was marked private and the single node resolved to null.#2859 —
read_private_postsusers could not see private postsA user who can
read_private_postsbut cannotedit_postscould not queryprivateposts.privatestatus for an authorized reader; theprivatebranch only ever denied, then fell through to theedit_postscheck.edit_postscheck before it ever consideredread_private_posts.The fix
Both gates now apply the same rules, derived from the status object and the post type's capability object:
publish, and any status whosepublicflag istrue(on apublic/publicly_queryablepost type), are visible to everyone.privaterequiresread_private_posts.edit_posts.Capability checks read the post type's capability object, so custom post type capabilities (
read_private_{cpt}s, etc.) are respected. The revision and auto-draft access logic is unchanged, and the owner-vs-capability precedence is preserved (a subscriber who owns a private post still cannot see it, matching the REST direction the existing tests document).For connections spanning multiple post types, a status is allowed only if the user can query it for every post type in the connection (the most restrictive choice).
A
graphql_allowed_post_statifilter is added so the queryable statuses for a connection can be adjusted by extensions.Tests
PostObjectConnectionQueriesTest,PostObjectQueriesTest).read_private_postsuser withoutedit_postssees private posts, while a plain subscriber still cannot.All four touched/adjacent suites stay green (
PostObjectConnectionQueriesTest,PostObjectQueriesTest,MediaItemQueriesTest,CustomPostTypeTest,RevisionTest,PreviewTest,PreviewContentNodesTest,ContentNodeInterfaceTest). PHPCS and PHPStan (level 8) are clean.Closes #2819
Closes #2859