Skip to content

fix: honor public post statuses and read_private_posts cap in post access checks#3966

Merged
jasonbahl merged 3 commits into
mainfrom
fix/2819-2859-public-post-stati
Jun 24, 2026
Merged

fix: honor public post statuses and read_private_posts cap in post access checks#3966
jasonbahl merged 3 commits into
mainfrom
fix/2819-2859-public-post-stati

Conversation

@jasonbahl

Copy link
Copy Markdown
Collaborator

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.

  • Connection gate (PostObjectConnectionResolver::sanitize_post_stati) only let publish and private through and fell every other status through to an edit_posts check, so a public custom status was stripped for anonymous and non-editor users.
  • Model gate (Post::is_post_private) only treated publish as public, so any other status (including a public custom status) was marked private and the single node resolved to null.

#2859read_private_posts users could not see private posts

A user who can read_private_posts but cannot edit_posts could not query private posts.

  • Connection gate never returned the private status for an authorized reader; the private branch only ever denied, then fell through to the edit_posts check.
  • Model gate applied a blanket edit_posts check before it ever considered read_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 whose public flag is true (on a public/publicly_queryable post type), are visible to everyone.
  • private requires read_private_posts.
  • All other statuses (draft, pending, future, trash, and custom non-public statuses) require 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_stati filter is added so the queryable statuses for a connection can be adjusted by extensions.

Tests

  • Public custom status is visible to anonymous users in a connection and as a single node (PostObjectConnectionQueriesTest, PostObjectQueriesTest).
  • A read_private_posts user without edit_posts sees private posts, while a plain subscriber still cannot.
  • A non-public custom status stays hidden from anonymous users in both paths, locking in the correct behavior from Not getting schedule (future) posts #3248 so this fix does not over-expose non-public statuses.

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

…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
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
wpgraphql-com Ready Ready Preview, Comment Jun 24, 2026 9:27pm

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 83.6%. Comparing base (8e91566) to head (55f5d2b).

Files with missing lines Patch % Lines
...c/Data/Connection/PostObjectConnectionResolver.php 94.7% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           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           
Flag Coverage Δ
wp-graphql-acf-wpunit-twentytwentyfive-single 77.5% <ø> (ø)
wp-graphql-wpunit-twentytwentyfive-single 84.7% <96.7%> (+<0.1%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
plugins/wp-graphql/src/Model/Post.php 87.8% <100.0%> (+0.4%) ⬆️
...c/Data/Connection/PostObjectConnectionResolver.php 96.7% <94.7%> (-0.4%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jasonbahl jasonbahl merged commit 39e5144 into main Jun 24, 2026
64 checks passed
@jasonbahl jasonbahl deleted the fix/2819-2859-public-post-stati branch June 24, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant