Merged
Conversation
This fixes a security vulnerability where authenticated user data could leak to public users through the object cache. The issue stemmed from WPGraphQL core calling wp_set_current_user(0) in has_authentication_errors() AFTER query execution but BEFORE the cache save hook fires. This caused is_user_logged_in() to return false even for authenticated requests. The fix: - Use AppContext->viewer instead of is_user_logged_in() to check auth state - AppContext->viewer is set at Request creation and doesn't change mid-request - Cache the is_object_cache_enabled() result per-request for consistency - Add Cache-Control: no-store header for authenticated requests See: wp-graphql/wp-graphql-jwt-authentication#38
This fixes a security vulnerability where authenticated user data could leak to public users through the object cache. The issue stemmed from WPGraphQL core calling wp_set_current_user(0) in has_authentication_errors() AFTER query execution but BEFORE the cache save hook fires. This caused is_user_logged_in() to return false even for authenticated requests. The fix: - Use AppContext->viewer instead of is_user_logged_in() to check auth state - AppContext->viewer is set at Request creation and doesn't change mid-request - Cache the is_object_cache_enabled() result per-request for consistency - Add Cache-Control: no-store header for authenticated requests See: wp-graphql/wp-graphql-jwt-authentication#38
WPGraphQL core calls wp_set_current_user(0) mid-request when no nonce is present (in has_authentication_errors), which caused is_user_logged_in() to return false even for authenticated requests. This allowed sensitive data (e.g., draft posts) to be cached and subsequently served to unauthenticated users. The fix: - Use AppContext->viewer->exists() instead of is_user_logged_in() to determine authentication state. AppContext->viewer is set once at Request creation and remains stable throughout the request lifecycle. - Include user ID in cache keys to ensure authenticated and unauthenticated requests produce separate cache entries. - Add Cache-Control: no-store header for authenticated requests to prevent network caches (CDN, Varnish) from caching sensitive responses. Includes wpunit and acceptance tests to prevent regression.
- Add second authenticated request to verify cache remains unpopulated across multiple auth requests - Add test step to verify authenticated users bypass existing cache entries populated by public requests - Improve test documentation with step-by-step comments These tests comprehensively validate that: 1. Authenticated requests never populate the object cache 2. Authenticated requests never read from the object cache 3. Public requests correctly use the cache 4. Complete isolation exists between auth and public caching
- Pin behat/gherkin to < 4.9 to fix i18n.php path issue with Codeception 4.x - Add gherkin: [] to codeception.dist.yml as extra safeguard - Update deprecated GitHub Actions to v4 - Fix deprecated set-output syntax
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.
Description
This PR fixes an issue where authenticated user data could be cached and subsequently served to public (unauthenticated) users.
The Problem
When a logged-in user makes a GraphQL request without a nonce (e.g., via a direct URL like
/graphql?query={posts(where:{status:DRAFT}){nodes{title}}}), the following sequence occurs:AppContext->vieweris set to the authenticated user (e.g., admin with ID 123)has_authentication_errors()runs: Since no nonce was provided, WPGraphQL core callswp_set_current_user(0)is_user_logged_in()to determine if caching is enabledAt step 4,
is_user_logged_in()returnsfalsebecausewp_set_current_user(0)was called in step 3. However, the query results from step 2 still contain authenticated data. This caused Smart Cache to incorrectly cache the authenticated response, which could then be served to public users.The Solution
Instead of using
is_user_logged_in()(which can change mid-request), we now useAppContext->viewerto determine the user's authentication state:AppContext->vieweris set once at Request creation and never changeswp_set_current_user(0)is called,AppContext->viewerstill reflects the original authenticated userChanges
src/Cache/Query.php$requestproperty to accessAppContext->viewerbuild_key()to useAppContext->viewer->IDinstead ofwp_get_current_user()->IDsrc/Cache/Results.phpis_object_cache_enabled()to checkAppContext->viewer->exists()instead ofis_user_logged_in()is_object_cache_enabledresult per-request to ensure consistent behaviorCache-Control: no-storeheader for authenticated requests to prevent network/CDN cachingsrc/Cache/Collection.php$this->requestbefore callingbuild_key()so it can accessAppContext->viewerTest Coverage
Added comprehensive test coverage:
tests/wpunit/AuthenticatedRequestCacheTest.php- Unit tests covering:AppContext->viewerexists (authenticated)Cache-Control: no-storeheader is set for authenticated requeststests/acceptance/AuthenticatedRequestCacheCest.php- Acceptance tests covering:How to Test
/graphql?query={posts(where:{status:DRAFT}){nodes{title status}}}Related
This fix addresses a quirk in WPGraphQL core where
wp_set_current_user(0)is called inafter_execute()— after the query has already executed with authenticated permissions. A separate issue will be opened in WPGraphQL core to address the timing of this authentication check.Note
Uses AppContext->viewer to disable caching for authenticated requests (and in cache keys), adds Cache-Control: no-store, and introduces tests to prevent draft/private data leaking to public users; updates CI and test configs.
Results: Determine auth viaAppContext->viewer(notis_user_logged_in()), cache the decision per-request, and setCache-Control: no-storefor authenticated requests; keep auth requests out of object cache.Query: Add$requestand useAppContext->viewer->IDinbuild_key()to segregate by user reliably.Collection: Set$this->requestbeforebuild_key()so cache keys include stable user context.wpunit/AuthenticatedRequestCacheTest.phpcovering auth vs public caching, header behavior, and sequential auth-state scenarios.AuthenticatedRequestCacheCest.phpverifying no leak of draft posts, public caching works, and auth bypasses cache.tests/acceptance.suite.ymlto useWPDb; add helper and config tweaks.actions/checkout@v4,actions/cache@v4,[email protected]) and fixGITHUB_OUTPUTusage.codeception.dist.yml) and composer dev setup/scripts; keep lockfile in sync.Written by Cursor Bugbot for commit 7d8f951. This will update automatically on new commits. Configure here.