Privilege Escalation Overview
CVE-2025-13820 is a critical vulnerability in the WordPress plugin wpDiscuz versions prior to 7.6.40 that allows unauthenticated attackers to escalate privileges via the Disqus OAuth integration, resulting in full account takeover on the affected WordPress site.
Vulnerability
| Software: | wpDiscuz |
| Type: | WordPress Plugin |
| Affected Versions: | < 7.6.40 |
| Affected Component: | Social Login (Disqus OAuth integration) |
| CVE ID: | CVE-2025-13820 |
| Vulnerability: | Privilege Escalation |
| Impact: | Unauthenticated attackers can impersonate existing WordPress users by abusing trusted OAuth identity data. |
| Attack Prerequisites: | – wpDiscuz Disqus social login enabled – User registration enabled |
| CVSS Score: | 9.1 (Critical) CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Published: | 2026-01-01 |
| Researcher: | wcraft |
Investigation
The root cause of this vulnerability lies in the Social Login implementation of wpDiscuz when the Disqus provider is enabled. Specifically, the issue occurs during the OAuth callback handling, where identity data returned by Disqus is trusted without sufficient validation or normalization.
The relevant logic is implemented inwpdiscuz/forms/wpdFormAttr/Login/SocialLogin.php,
within the disqusLoginCallBack() method, which is responsible for exchanging the OAuth authorization code for user identity data and completing the login flow:
$userID = $disqusAccesTokenData["user_id"];
$accesToken = $disqusAccesTokenData["access_token"];
$disqusGetUserDataURL = "https://disqus.com/api/3.0/users/details.json";
$disqusGetUserDataAttr = [
"access_token" => $accesToken,
"api_key" => $this->generalOptions->social["disqusPublicKey"],
];
$getDisqusUserResponse = wp_remote_get($disqusGetUserDataURL, ["body" => $disqusGetUserDataAttr]);
if (is_wp_error($getDisqusUserResponse)) {
$this->redirect($postID, $getDisqusUserResponse->get_error_message());
}
$disqusUserData = json_decode(wp_remote_retrieve_body($getDisqusUserResponse), true);
if (isset($disqusUserData["code"]) && $disqusUserData["code"] != 0) {
$this->redirect($postID, $disqusUserData["response"]);
}
$disqusUser = $disqusUserData["response"];
$disqusUser["user_id"] = $userID;
$uID = Utils::addUser($disqusUser, "disqus");PHPDuring the callback process, wpDiscuz performs a server-side request tohttps://disqus.com/api/3.0/users/details.json
using wp_remote_get() to retrieve the authenticated Disqus user’s profile data. This response is then used to create or map a corresponding WordPress user account.
Because this request is executed entirely server-side, the returned identity data is never exposed to the client and does not pass through the browser. As a result, tools such as Burp Suite cannot intercept or tamper with this data in transit, and the vulnerability cannot be exploited via direct request manipulation (e.g., POST parameter injection). This informs us that exploitation instead depends on abusing the legitimate OAuth workflow and the plugin’s trust assumptions.
This request is subsequently processed by the addUser() function located in wpdiscuz/forms/wpdFormAttr/Login/Utils.php. Before any user record is created or updated, the Disqus user data returned from the OAuth flow is normalized and mapped into a WordPress-compatible structure:
private static function sanitizeDisqusUser($disqusUser) {
$userData = [
"user_login" => self::saitizeUsername($disqusUser["username"]),
"first_name" => "",
"last_name" => "",
"display_name" => $disqusUser["name"],
"user_url" => $disqusUser["profileUrl"],
"user_email" => $disqusUser["email"],
"provider" => "disqus",
"social_user_id" => $disqusUser["user_id"],
"avatar" => isset($disqusUser["avatar"]["permalink"]) ? $disqusUser["avatar"]["permalink"] : "",
];
return $userData;
}PHPNotably, the user_email field is populated directly from the Disqus API response without any verification that the email address is owned or previously validated by the authenticating WordPress user.
Once sanitized, this data is used to either update an existing WordPress user or create a new one. The decision is made based solely on whether the supplied email address already exists in the WordPress user table.
if ($userData) {
if ($userID = email_exists($userData["user_email"])) {
$userData["ID"] = $userID;
$userData["status"] = "update";
} else {
$userData["role"] = get_option("default_role");
$userID = $userData["ID"] = wp_insert_user($userData);
}
if ($userID && !is_wp_error($userID)) {
self::updateUserData($userData);
update_user_meta($userID, wpdFormConst::WPDISCUZ_SOCIAL_AVATAR_KEY, $userData["avatar"]);
}
}PHPIf a matching email address is found, the plugin assumes the user already exists and proceeds to update that account. Otherwise, a new user is created using the site’s default role.
This is the critical decision point. No additional validation is performed to confirm that the Disqus account and the existing WordPress account represent the same individual. The email address alone is treated as a trusted, globally unique identifier.
After the user record is resolved, the login process is finalized by calling setCurrentUser() in SocialLogin.php, which establishes the authenticated session for the resolved user ID.
private function setCurrentUser($userID) {
$user = get_user_by("id", $userID);
wp_set_current_user($userID, $user->user_login);
wp_set_auth_cookie($userID, (bool)$this->generalOptions->social["rememberLoggedinUser"]);
do_action("wp_login", $user->user_login, $user);
}PHPAt this stage, the requester is fully logged into the WordPress account associated with the matching email address, regardless of whether they legitimately control that account.
The vulnerability arises from the implicit trust placed in the email value supplied by the Disqus OAuth response. The implementation assumes that the email address used during Disqus account creation is both verified and uniquely bound to the authenticating user. This assumption is not enforced or validated within wpDiscuz during the sign-up flow, allowing an attacker to authenticate via Disqus using an email address already associated with a privileged WordPress account and thereby hijack it. The only caveat is if the email is already registered with Disqus.com, in which case the email cannot be used.
Exploit (PoC)
- Install & activate wpDiscuz < 7.6.40 from the WordPress.org repo. (e.g.,
wp plugin install wpdiscuz --version=7.6.39 --activate) - Ensure Comments are enabled on posts. If using an FSE theme and the wpDiscuz comment form is not displaying, then adding the line
<!-- wp:comments {"legacy":true} /-->via the Code Editor view may be necessary. - Enable the
Disqus Login Buttonat/wp-admin/admin.php?page=wpdiscuz_options_page&wpd_tab=social - Configure the API Keys as per Disqus’ instructions. Creating a free Disqus account will be required.
- Create a dummy admin account on the WordPress account (e.g.,
wp user create danielr [email protected] --role=administrator) - Navigate to a Post on the site, log into the comment form using the
Connect withDisqusbutton. You will be redirected to a Disqus login page. Click “Need an account?” link, and fill out the required details, being sure to use the same dummy email you used for the WordPress site’s dummy admin account. - You will then automatically be logged into the WordPress admin’s account on the site, and can navigate to
/wp-adminslug to access the WP Admin features with admin privileges.
Mitigation
The vulnerability is fully addressed by updating to wpDiscuz 7.6.40 or later. In the patched release, the wpDiscuz maintainers changed how Disqus identities are mapped by no longer trusting the email address returned by the OAuth response. Instead, the plugin now derives a synthetic email in the form of [email protected], eliminating email-based account collisions.
Below are two example platform-level virtual patches (vpatches) that demonstrate how this issue can be mitigated without modifying plugin code. These examples are intentionally generic for clarity. In large-scale environments, running such logic unconditionally on all outgoing HTTP requests could be unnecessarily expensive. In practice, the overhead can be reduced to near zero by applying additional constraints—such as limiting execution to AJAX requests (DOING_AJAX) and further narrowing the scope to requests associated with the wpd_login_callback OAuth flow.
Mitigation 1: Disable Disqus login
Disabling Disqus login can be done directly through the wpDiscuz settings UI. However, this approach may not be practical in large-scale or managed hosting environments, where mitigation needs to be applied quickly and consistently across many sites.
As an alternative, Disqus-based authentication can be programmatically blocked by intercepting the OAuth callback request. This can be achieved by inspecting $_REQUEST and denying requests where action equals wpd_login_callback and provider equals disqus, returning an HTTP 403 response when these conditions are met.
This approach effectively prevents the vulnerable login flow from executing without requiring changes to individual site configurations.
<?php
/**
* MU vpatch: Mitigates CVE-2025-13820 by disabling the wpDiscuz Disqus
* social login callback to prevent account takeover via email collision
* during Disqus OAuth authentication.
*
* This is a targeted, minimal mitigation written as part of active CVE
* research and incident response work. It is intended to reduce exposure
* risk while upstream fixes are pending or under evaluation.
*
*
* Author: danielr
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* This assumes the callback hits a request like:
* ?action=wpd_login_callback&provider=disqus
*
* If your wpDiscuz version uses different param names, tweak below.
*/
add_action( 'init', function () {
$action = isset( $_REQUEST['action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : '';
$provider = isset( $_REQUEST['provider'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['provider'] ) ) : '';
if ( $action === 'wpd_login_callback' && $provider === 'disqus' ) {
wp_die(
esc_html__( 'Disqus login is temporarily disabled on this site.', 'wpdiscuz-vpatch' ),
esc_html__( 'Login disabled', 'wpdiscuz-vpatch' ),
array( 'response' => 403 )
);
}
}, 1 );
PHPMitigation 2: Normalize Disqus identity fields during OAuth (email + user_login)
A second mitigation approach is to apply a virtual patch (vpatch) that normalizes identity data returned by the Disqus OAuth flow before wpDiscuz uses it to create or map a WordPress user. This mirrors the spirit of the upstream fix (removing trust in the provider-supplied email), while also hardening the username mapping to prevent collisions.
wpDiscuz ultimately derives two critical WordPress identifiers from the Disqus profile payload:
response.email– mapped touser_emailresponse.username– mapped touser_login
This vpatch intercepts the Disqus users/details.json response server-side and rewrites both fields deterministically:
- Email normalization: replace the provider-returned email with a synthetic, namespaced email derived from the Disqus numeric user ID:
disqus_<user_id>@disqus.com - Login normalization: replace the provider-returned username with a stable, namespaced identifier derived from the same user ID:
disqus_<md5(user_id)>
By doing this, wpDiscuz no longer uses potentially collision-prone identity attributes (like an email address chosen during signup or a mutable username) when it decides whether to update an existing WordPress user via email_exists(). Instead, each Disqus account maps to a unique, deterministic WordPress identity, preventing account takeover via email-based user matching.
This approach preserves Disqus login functionality while eliminating the ambiguous account-binding behavior that enables the vulnerability.
<?php
/**
* MU vpatch: Mitigates CVE-2025-13820 by rewriting Disqus OAuth identity fields
* in JSON API responses before they are consumed by WordPress.
*
* This patch enforces a deterministic, collision-resistant identity mapping
* by namespacing the Disqus email address and replacing the username with a
* stable hash derived from the Disqus user ID. This prevents account takeover
* scenarios caused by email or username collisions during social login.
*
* This is a targeted, minimal mitigation written as part of active CVE
* research and incident response work. It is intended to reduce exposure
* risk while upstream fixes are pending or under evaluation.
*
*
* Author: danielr
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_filter( 'http_response', function( $response, $parsed_args, $url ) {
$host = wp_parse_url( $url, PHP_URL_HOST );
if ( ! $host || stripos( $host, 'disqus.com' ) === false ) {
return $response;
}
$data = json_decode( $response['body'], true );
if ( isset( $data['response']['email'] ) ) {
$data['response']['email'] = 'disqus_' . $data['response']['id'] . '@disqus.com';
$data['response']['username'] = 'disqus_' . md5( (string) $data['response']['id'] );
$response['body'] = wp_json_encode( $data );
}
return $response;
}, 10, 3 );
PHPFinal Thoughts
At a glance, the wpDiscuz Disqus login flow appears well-structured: OAuth is used correctly, requests are performed server-side, and user data is passed through a dedicated sanitization layer. Nothing here immediately signals a traditional injection point or malformed input vulnerability.
The issue emerges only when the identity assumptions are examined more closely. By treating an externally supplied email address as a globally trusted identifier, the plugin collapses the boundary between third-party identity and local authorization. That assumption, while seemingly reasonable, is what ultimately enables account takeover.
This vulnerability is a reminder that many high-impact issues do not come from obviously unsafe code, but from logic that appears correct until its trust model is questioned. These are the kinds of bugs that often survive code review, pass initial testing, and quietly ship to production, because they look safe to me 👍.
Leave a Reply