
Defensive analysis, patch breakdown, and detection scanner for CVE-2026-14378 (WordPress DevKit Pro Plugin <= 2.3.0).
CVE-2026-14378 is a critical Unauthenticated Authentication Bypass (CVSS 9.8) in the DevKit Pro plugin for WordPress, affecting all versions up to and including 2.3.0.
The plugin includes a developer user-switching mechanism. When an admin "switches" to another user account, it stores the admin's ID in a cookie named original_user_id. The flaw: the plugin renders a "switch back" HTML form on any page whenever that cookie is present — including for unauthenticated visitors who set the cookie manually. Worse, the nonce verification step checks the cookie user's admin capability, not the caller's session — so the server happily hands out an authenticated admin session cookie to anyone who sends the right POST.
Net result: zero credentials required for full WordPress admin takeover.
| Attribute | Details |
|---|---|
| CVE ID | CVE-2026-14378 |
| Vulnerability Class | Improper Authentication (CWE-287) |
| CVSS v3.1 Score | 9.8 (Critical) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Affected Software | DevKit Pro (dplugins) WordPress Plugin |
| Vulnerable Versions | <= 2.3.0 |
| Patched Version | 2.3.1 |
| Disclosure Date | 02 Oct 2026 |
DevKit Pro includes a developer helper that lets site admins "switch" into other user accounts to test permissions. When the admin uses the switch:
Set-Cookie: original_user_id=1isset($_COOKIE['original_user_id'])wp_footer() with a hidden POST form containing a fresh nonceadmin-post.php?action=revert_switchwp_set_auth_cookie($user_id) to restore the original sessionThe wp_footer hook handler:
// DevKit Pro <= 2.3.0 — render_switch_back_bar()
public function render_switch_back_bar() {
// FLAW: Only checks if cookie exists — no session validation!
if ( isset( $_COOKIE['original_user_id'] ) ) {
$user_id = (int) $_COOKIE['original_user_id'];
$nonce = wp_create_nonce( 'devkit_revert_switch_' . $user_id );
echo '<div id="devkit-pro-switch-back" class="devkit-switch-bar" style="display:none;">';
echo ' <form id="devkit-revert-form" action="' . admin_url('admin-post.php') . '" method="POST">';
echo ' <input type="hidden" name="action" value="revert_switch" />';
echo ' <input type="hidden" name="_wpnonce" value="' . $nonce . '" />';
echo ' <input type="hidden" name="target_user_id" value="' . $user_id . '" />';
echo ' </form>';
echo '</div>';
echo '<!-- DevKit Pro 2.3.0 Switch Component Active -->';
}
}
The POST handler that processes the form submission:
// DevKit Pro <= 2.3.0 — handle_revert_switch()
public function handle_revert_switch() {
$user_id = (int) $_POST['target_user_id'];
$nonce = sanitize_text_field( $_POST['_wpnonce'] );
if ( ! $this->verify_nonce_and_capability( $user_id, $nonce ) ) {
wp_die( 'Unauthorized' );
}
wp_set_current_user( $user_id );
wp_set_auth_cookie( $user_id ); // <-- Grants authenticated session to caller
wp_redirect( admin_url() );
exit;
}
private function verify_nonce_and_capability( $user_id, $nonce ) {
if ( ! wp_verify_nonce( $nonce, 'devkit_revert_switch_' . $user_id ) ) {
return false;
}
// CRITICAL FLAW: Checks the cookie user's capability, not the caller's!
return user_can( $user_id, 'manage_options' );
}
user_can( $user_id, 'manage_options' ) answers the question: "Does user #1 have the manage_options capability?"
The answer for user #1 (the first-created WordPress admin) is always true.
It should be asking: "Does the person making this HTTP request have the manage_options capability?"
The correct check is current_user_can('manage_options') which would return false for an unauthenticated visitor.
This entire walkthrough was performed and verified against a live WordPress instance running in a local Podman container (http://localhost:8080) with DevKit Pro 2.3.0 active.
To reproduce this locally, you need:
Quick Podman lab setup:
# Start MariaDB
podman run -d --name wp-db \
-e MYSQL_ROOT_PASSWORD=rootpass \
-e MYSQL_DATABASE=wordpress \
-e MYSQL_USER=wpuser \
-e MYSQL_PASSWORD=wppass \
mariadb:10.6
# Start WordPress
podman run -d --name wp-app \
-p 8080:80 \
--link wp-db:mysql \
-e WORDPRESS_DB_HOST=mysql \
-e WORDPRESS_DB_NAME=wordpress \
-e WORDPRESS_DB_USER=wpuser \
-e WORDPRESS_DB_PASSWORD=wppass \
wordpress:latest
After WordPress is initialized (http://localhost:8080/wp-admin/install.php), install and activate DevKit Pro 2.3.0 via the plugins menu.
Before sending any attack payload, confirm the plugin is present and determine its version by fetching the plugin readme:
Request:
GET /wp-content/plugins/devkit-pro/readme.txt HTTP/1.1
Host: localhost:8080
Response (HTTP 200):
=== DevKit Pro ===
Requires at least: 5.0
Tested up to: 6.8
Requires PHP: 7.4
Stable tag: 2.3.0
License: GPLv2 or later
Version 2.3.0 is within the affected range <= 2.3.0. Proceed.