{"id":"CVE-2026-54768","aliases":["GHSA-jhh7-832h-f8hv"],"url":"https://o3.security/vulnerability/CVE-2026-54768","summary":"WPGraphQL has deprecated `user` field on SendPasswordResetEmailPayload that leaks user existence + profile (defeats explicit anti-enumeration design)","details":"## Summary\n\nThe `sendPasswordResetEmail` mutation in WPGraphQL is explicitly designed to prevent user enumeration. The resolver in `src/Mutation/SendPasswordResetEmail.php` states in a code comment:\n\n`// We obsfucate the actual success of this mutation to prevent user enumeration.`\n\nThe mutation always returns `success: true` regardless of whether the supplied username/email belongs to an existing user. The intended public output field is only `success: Boolean`.\n\nHowever, a deprecated `user` field is still registered on the `SendPasswordResetEmailPayload` output type in `src/Deprecated.php` (lines 433-450). This deprecated field resolves to a full `User` object when the supplied username/email corresponds to an existing author-class user, and `null` otherwise — completely undermining the anti-enumeration design.\n\nThe `@todo remove in 3.0.0` comment acknowledges the field is scheduled for removal, but it remains active in all 2.x releases, including current 2.14.1.\n\nDiscovered via source code review on May 29, 2026.\n\n## Details\n\nThe mutation resolver in `src/Mutation/SendPasswordResetEmail.php`:\n\n```php\n$payload = ['success' => true, 'id' => null];\n$user_data = self::get_user_data($input['username']);\nif (!$user_data) {\n    graphql_debug(...);\n    return $payload;  // id stays null\n}\n// ...send email, then...\nreturn ['id' => $user_data->ID, 'success' => true];\n```\n\nThe intended public output field is only `success`. The `id` is internal-only state for downstream resolvers.\n\n`src/Deprecated.php` registers an additional `user` field on the same payload type:\n\n```php\nregister_graphql_field(\n    'SendPasswordResetEmailPayload',\n    'user',\n    [\n        'type' => 'User',\n        'deprecationReason' => static function () { return __('This field will be removed...'); },\n        'resolve' => static function ($payload, $args, AppContext $context) {\n            return !empty($payload['id'])\n                ? $context->get_loader('user')->load_deferred($payload['id'])\n                : null;\n        },\n    ],\n);\n```\n\nThis field reads the internal `$payload['id']` and resolves it through the standard user loader. The User Model's `allowed_restricted_fields` policy permits unauthenticated reads of public author fields (`databaseId`, `name`, `firstName`, `lastName`, `slug`, `description`, `uri`, `url`).\n\n## PoC\n\n```graphql\nmutation EnumerateUser {\n  sendPasswordResetEmail(input: { username: \"victim@example.com\" }) {\n    success\n    user {\n      databaseId\n      name\n      firstName\n      lastName\n      slug\n      description\n      uri\n    }\n  }\n}\n```\n\nBehavior:\n- Non-existing user/email → `data.sendPasswordResetEmail.user` is `null`\n- - Existing author-class user → `data.sendPasswordResetEmail.user` is a full User object with the listed fields populated\n- - `success` always returns `true`, preserving the appearance of obfuscation — the deprecated `user` field is the leak\n## Impact\n\n1. **Username/email enumeration:** unauthenticated attacker can verify whether any username or email is registered, with no WPGraphQL-side rate limiting\n2. 2. **Profile disclosure for author-class users:** for any user with published posts (including editors and administrators), the attacker obtains `databaseId`, `name`, `firstName`, `lastName`, `slug`, `description` (user bio), `uri` — substantially more than mere existence\n3. 3. **Bypasses partial hardening:** sites that disabled the REST API user endpoint, the user XML sitemap, and `?author=N` author redirects may still be vulnerable through this WPGraphQL path\n4. 4. **Spearphishing setup:** firstName/lastName/description for authors provides personalized phishing material\n## Recommended fix\n\nEither remove the deprecated `user` field entirely (advance the existing `@todo remove in 3.0.0`) or change the resolver to always return `null`:\n\n```diff\n'resolve' => static function ($payload, $args, AppContext $context) {\n-    return !empty($payload['id']) ? $context->get_loader('user')->load_deferred($payload['id']) : null;\n- +    // Always null — this deprecated field previously leaked user existence,\n- +    // undermining the anti-enumeration design of the sendPasswordResetEmail mutation.\n- +    return null;\n- },\n- ```\nDefense in depth — change the mutation resolver itself to not populate `$payload['id']` on real success:\n\n```diff\nreturn [\n-    'id'      => $user_data->ID,\n- +    'id'      => null,\n-      'success' => true,\n- ];\n- ```\n\nLuke Granto — independent security researcher operating in good faith. Discovery via source code review of wp-graphql/wp-graphql v2.14.1, approximately 15 minutes from `git clone` to confirmed bug. No live exploitation against any third-party deployment.","published":"2026-07-31T22:24:40.639Z","modified":"2026-10-28T02:30:54.232533683Z","cvss":null,"epss":{"score":0.00344,"percentile":0.27813,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"wp-graphql/wp-graphql","fixedVersion":null}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/wp-graphql/wp-graphql/releases/tag/wp-graphql/v2.15.1"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/54xxx/CVE-2026-54768.json"},{"type":"ADVISORY","url":"https://github.com/wp-graphql/wp-graphql/security/advisories/GHSA-jhh7-832h-f8hv"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-54768"},{"type":"PACKAGE","url":"https://github.com/wp-graphql/wp-graphql"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-28T02:30:54.232533683Z"}}