Aug 1, 2026·7 min read·41 visits
A deprecated field resolver in WPGraphQL's password reset mutation leaks user existence and profile metadata, undermining anti-enumeration controls.
An observable response discrepancy vulnerability in WPGraphQL versions 2.0.0 through 2.15.0 allows unauthenticated remote attackers to enumerate users and extract public profile metadata. Although the password reset mutation is designed to return a uniform success response to prevent enumeration, a legacy deprecated field resolver bypasses this mechanism by resolving the associated user profile if the target account exists.
The vulnerability resides in WPGraphQL, an open-source WordPress plugin that exposes a GraphQL API for interacting with WordPress sites. Specifically, the flaw exists within the implementation of the sendPasswordResetEmail mutation and its corresponding output type payload. Although the mutation was designed to prevent user enumeration, legacy field definitions undermine this defense.
By default, password reset mechanisms should not reveal whether a provided username or email address exists in the database. When a system provides distinct responses based on the validity of an identity, it introduces an observable response discrepancy. WPGraphQL intended to mask this by always returning a success indicator.
However, a deprecated GraphQL field resolver on the response payload class fails to respect this design. Unauthenticated attackers can request this deprecated field to determine the existence of any user on the system. If the user exists, the application leaks their public profile data; if not, it returns null.
This behavior exposes a severe information disclosure vector on WordPress sites running WPGraphQL. Because the GraphQL endpoint is publicly accessible by default, this vulnerability exposes system configurations, user handles, and administrative metadata.
The root cause of this vulnerability lies in an architectural misalignment between the password reset mutation resolver and a legacy field definition. The primary resolver is defined in src/Mutation/SendPasswordResetEmail.php. To prevent user enumeration, this resolver contains logic to always output a status of true, while conditionally populating an internal payload parameter.
When a mutation request is processed, the resolver checks if the requested username or email belongs to a registered WordPress user. If the user exists, the internal array $payload is populated with the user's database ID. If the user does not exist, the ID field remains set to null. Crucially, the public-facing schema for the payload was modified to hide this ID, returning only the success status to the client.
However, the file src/Deprecated.php registers a legacy field named user directly on the SendPasswordResetEmailPayload type. The field resolver for this legacy property extracts the internal database ID from the parent payload. If the ID is not null, the resolver triggers a deferred load of the corresponding User object, returning it to the query execution pipeline.
Because the GraphQL engine processes fields requested by the client, an attacker can explicitly request the deprecated user field. WPGraphQL allows unauthenticated clients to read public fields of user objects, meaning the full user metadata is returned to the client when a matching account is found. This behavior bypasses the protection and allows systematic enumeration.
Analyzing the vulnerable code path demonstrates how the legacy resolver exposes the hidden internal state. In src/Deprecated.php, the legacy user field is registered on the payload type with the following resolver function:
// src/Deprecated.php (vulnerable state)
register_graphql_field(
'SendPasswordResetEmailPayload',
'user',
[
'type' => 'User',
'deprecationReason' => static function () { return __('This field will be removed...'); },
'resolve' => static function ($payload, $args, AppContext $context) {
// Vulnerability: Resolves the user if the internal 'id' is populated
return !empty($payload['id'])
? $context->get_loader('user')->load_deferred($payload['id'])
: null;
},
],
);The mutation resolver in src/Mutation/SendPasswordResetEmail.php populates the ID when a match is found, believing this ID is internal and unexposed to the public Schema:
// src/Mutation/SendPasswordResetEmail.php (vulnerable state)
$payload = ['success' => true, 'id' => null];
$user_data = self::get_user_data($input['username']);
if (!$user_data) {
return $payload; // ID remains null, success is true
}
// ... executes password reset process ...
return [
'id' => $user_data->ID, // ID is exposed to the deprecated resolver payload
'success' => true,
];To resolve this flaw, the developers decoupled the internal payload representation from the database ID entirely. The patch hardcodes the deprecated resolver to always return null, and ensures the mutation resolver no longer leaks the ID in its return array. Below is the patch implementation:
// src/Deprecated.php (patched state)
register_graphql_field(
'SendPasswordResetEmailPayload',
'user',
[
'type' => 'User',
'deprecationReason' => static function () { return __('This field will be removed...'); },
'resolve' => static function ($payload, $args, AppContext $context) {
// Patch: Always return null to prevent information exposure
return null;
},
],
);Additionally, the mutation payload was modified to prevent returning the database ID in the successful execution return statement:
// src/Mutation/SendPasswordResetEmail.php (patched state)
return [
'id' => null, // Hardcoded to null to prevent resolution down the chain
'success' => true,
];This fix is complete and robust because it removes the data propagation path completely. No variant attacks can re-exploit this code path since the internal database ID never reaches any schema resolvers.
Exploitation of this vulnerability requires no authentication and can be performed via standard HTTP POST requests directed at the /graphql endpoint. An attacker crafts a GraphQL query that targets the sendPasswordResetEmail mutation and requests the deprecated user field nested within the payload.
The payload is structured to request public profile fields of the resolved user, such as databaseId, name, firstName, lastName, and slug. A standard attack payload looks as follows:
mutation EnumerateUser {
sendPasswordResetEmail(input: { username: "target-user" }) {
success
user {
databaseId
name
slug
}
}
}If the queried username does not exist on the system, the server returns a JSON response indicating success, but the user block is null. This mimics a successful submission without leaking state:
{
"data": {
"sendPasswordResetEmail": {
"success": true,
"user": null
}
}
}If the queried username belongs to an existing user, the deprecated resolver executes and returns the populated User object. The attacker receives detailed account information, confirming the account existence and harvesting username slugs:
{
"data": {
"sendPasswordResetEmail": {
"success": true,
"user": {
"databaseId": 4,
"name": "Jane Doe",
"slug": "jane-doe"
}
}
}
}The impact of this information disclosure vulnerability is high, particularly for installations relying on obscured administrative handles to prevent credential-based attacks. By extracting user database IDs and slugs, attackers bypass standard security mechanisms designed to limit exposure.
WordPress hardening guidelines frequently recommend disabling the default WP REST API users endpoint (/wp-json/wp/v2/users) and blocking the standard ?author=N query redirection parameter. This vulnerability completely bypasses those mitigations, restoring full enumeration capability via the GraphQL API interface.
The harvested data simplifies brute-force attacks and credential stuffing campaigns, since attackers can construct exact target lists. Furthermore, public fields like user descriptions or bios provide context-rich information that can be utilized to craft high-credibility spearphishing messages targeting specific administrators.
The CVSS v4.0 base score is rated at 6.9, reflecting network accessibility with low complexity and no privilege requirements. While confidentiality is impacted, the vulnerability does not directly permit data modification or service interruption.
The primary and recommended remediation strategy is to upgrade the WPGraphQL plugin to version 2.15.1 or newer. This update disables the data leak in both the deprecated field handler and the mutation payload returning process.
When an immediate upgrade is not feasible, administrators can apply a manual hotfix by editing the affected PHP source files. In src/Deprecated.php, replace the return statement inside the user field resolver with return null;. In src/Mutation/SendPasswordResetEmail.php, modify the successful return array to assign 'id' => null.
Detection of exploitation attempts can be performed by analyzing web server logs and HTTP request bodies. Intrusion detection systems (IDS) and web application firewalls (WAF) can inspect POST requests to the /graphql route. A signature can be deployed to flag or block requests that call sendPasswordResetEmail while requesting the nested user field.
# Web Application Firewall detection pattern
mutation.*sendPasswordResetEmail.*user\s*\{Any request matching this pattern is highly anomalous and indicates potential enumeration activity, as legitimate applications have no valid reason to query a deprecated field inside a password reset transaction.
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| Product | Affected Versions | Fixed Version |
|---|---|---|
WPGraphQL wp-graphql | >= 2.0.0, < 2.15.1 | 2.15.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-204 |
| Attack Vector | Network |
| CVSS v4.0 | 6.9 (Medium) |
| Exploit Status | Proof of Concept Available |
| CISA KEV Status | Not Listed |
| Ransomware Association | No Known Use |
| Impact | Unauthenticated Username Enumeration and Metadata Disclosure |
The product behaves differently when queries contain valid values versus invalid values, allowing attackers to determine database record existence.
A path traversal vulnerability in Shiny for Python (posit-dev/py-shiny) versions 1.4.0 through 1.6.3 allows unauthenticated remote attackers to read arbitrary files and traverse directories via crafted _state_id_ query parameters.
A Stored Cross-Site Scripting vulnerability in @tinacms/web-components prior to version 0.2.1 allows low-privileged content authors to execute arbitrary JavaScript code in the context of website visitors via unsanitized URL attributes in custom Markdown rendering components.
A critical origin validation flaw in TinaCMS admin preview allows unauthenticated attackers to bypass cross-origin postMessage checks and execute unauthorized GraphQL queries and mutations under an authenticated editor's context.
@tinacms/cli prior to version 3.0.0 dynamically constructs client source files using string interpolation without properly sanitizing runtime configuration variables. An attacker with permissions to create a branch or pull request can inject arbitrary JavaScript statements via a crafted Git ref name, leading to execution during automated build processes.
Nginx UI versions 2.2.0 through 2.5.10 fail to properly configure Gin framework trusted proxies when deployed behind a reverse proxy. This causes all incoming HTTP requests to be attributed to the loopback IP (127.0.0.1), enabling IP allowlist bypass and global authentication lockouts.
Nginx UI versions 2.5.0 through 2.5.10 contain an uncontrolled resource consumption vulnerability in the node authentication handler. Unauthenticated remote attackers can exhaust host disk storage and I/O resources by submitting large HTTP request bodies to node-signature endpoints prior to cryptographic signature validation.