{"id":"GHSA-jhh7-832h-f8hv","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' =\u003e true, 'id' =\u003e 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' =\u003e $user_data-\u003eID, 'success' =\u003e 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' =\u003e 'User',\n        'deprecationReason' =\u003e static function () { return __('This field will be removed...'); },\n        'resolve' =\u003e static function ($payload, $args, AppContext $context) {\n            return !empty($payload['id'])\n                ? $context-\u003eget_loader('user')-\u003eload_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' =\u003e static function ($payload, $args, AppContext $context) {\n-    return !empty($payload['id']) ? $context-\u003eget_loader('user')-\u003eload_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'      =\u003e $user_data-\u003eID,\n- +    'id'      =\u003e null,\n-      'success' =\u003e 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.","aliases":["CVE-2026-54768"],"modified":"2026-07-31T22:41:15.780601Z","published":"2026-07-31T22:24:29Z","database_specific":{"nvd_published_at":null,"cwe_ids":["CWE-204"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-07-31T22:24:29Z"},"references":[{"type":"WEB","url":"https://github.com/wp-graphql/wp-graphql/security/advisories/GHSA-jhh7-832h-f8hv"},{"type":"PACKAGE","url":"https://github.com/wp-graphql/wp-graphql"},{"type":"WEB","url":"https://github.com/wp-graphql/wp-graphql/releases/tag/wp-graphql/v2.15.1"}],"affected":[{"package":{"name":"wp-graphql/wp-graphql","ecosystem":"Packagist","purl":"pkg:composer/wp-graphql/wp-graphql"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"2.6.0"}]}],"versions":["0.0.2","0.0.20","0.0.21","0.0.22","0.0.23","0.0.24","0.0.25","0.0.26","0.0.27","0.0.28","0.0.29","0.0.3","0.0.30","0.0.31","0.0.5","1.2.2","1.27.2","v0.0.10","v0.0.11","v0.0.12","v0.0.13","v0.0.14","v0.0.15","v0.0.16","v0.0.17","v0.0.18","v0.0.19","v0.0.32","v0.0.33","v0.0.34","v0.0.6","v0.0.7","v0.0.8","v0.0.9","v0.1.0","v0.1.1","v0.1.2","v0.1.3","v0.1.4","v0.10.0","v0.10.1","v0.10.2","v0.10.3","v0.11.0","v0.12.0","v0.12.1","v0.12.2","v0.12.3","v0.13.0","v0.13.1","v0.13.2","v0.13.3","v0.14.0","v0.15.0","v0.15.1","v0.15.2","v0.15.3","v0.15.4","v0.15.5","v0.15.6","v0.2.0","v0.2.1","v0.2.2","v0.2.3","v0.3.0","v0.3.01","v0.3.2","v0.3.3","v0.3.4","v0.3.5","v0.3.6","v0.3.7","v0.3.8","v0.4.0","v0.4.1","v0.4.2","v0.4.3","v0.5.0","v0.5.1","v0.6.0","v0.6.1","v0.6.2","v0.6.3","v0.7.0","v0.7.1","v0.8.0","v0.8.1","v0.8.2","v0.8.3","v0.8.4","v0.9.0","v0.9.1","v1.0","v1.0.1","v1.0.2","v1.0.3","v1.0.4","v1.0.5","v1.1.0","v1.1.1","v1.1.2","v1.1.3","v1.1.4","v1.1.5","v1.1.6","v1.1.7","v1.1.8.0","v1.10.0","v1.11.0","v1.11.1","v1.11.2","v1.11.3","v1.12.0","v1.12.1","v1.12.2","v1.12.3","v1.13.0","v1.13.1","v1.13.10","v1.13.2","v1.13.3","v1.13.4","v1.13.5","v1.13.6","v1.13.7","v1.13.8","v1.13.9","v1.14.0","v1.14.10","v1.14.2","v1.14.3","v1.14.4","v1.14.5","v1.14.6","v1.14.7","v1.14.8","v1.14.9","v1.15.0","v1.16.0","v1.17.0","v1.18.0","v1.18.1","v1.18.2","v1.19.0","v1.2.0","v1.2.1","v1.2.3","v1.2.4","v1.2.5","v1.2.6","v1.20.0","v1.21.0","v1.22.0","v1.22.1","v1.23.0","v1.24.0","v1.25.0","v1.26.0","v1.27.0","v1.27.1","v1.28.0","v1.28.1","v1.29.0","v1.29.1","v1.29.2","v1.29.3","v1.3.0","v1.3.1","v1.3.10","v1.3.2","v1.3.3","v1.3.4","v1.3.5","v1.3.6","v1.3.7","v1.3.8","v1.3.9","v1.30.0","v1.31.0","v1.31.1","v1.32.0","v1.32.1","v1.4.0","v1.4.1","v1.4.2","v1.4.3","v1.4.7","v1.5.0","v1.5.1","v1.5.2","v1.5.3","v1.5.4","v1.5.5","v1.5.6","v1.5.7","v1.5.8","v1.5.9","v1.6.0","v1.6.1","v1.6.10","v1.6.11","v1.6.12","v1.6.2","v1.6.3","v1.6.4","v1.6.5","v1.6.6","v1.6.7","v1.6.8","v1.6.9","v1.7.0","v1.7.1","v1.7.2","v1.8.0","v1.8.1","v1.8.2","v1.8.3","v1.8.4","v1.8.5","v1.8.6","v1.8.7","v1.9.0","v1.9.1","v2.0.0","v2.0.0-beta.1","v2.0.0-beta.2","v2.1.0","v2.1.1","v2.2.0","v2.3.0","v2.3.3","v2.3.6","v2.3.8","v2.5.0","v2.5.1","v2.5.2","v2.5.3","v2.5.4","v2.6.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-jhh7-832h-f8hv/GHSA-jhh7-832h-f8hv.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N"}]}