Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 21, 2026

CVE-2026-7615: Widget Context <= 1.3.3 – Cross-Site Request Forgery to Settings Update via 'wl' Parameter (widget-context)

CVE ID CVE-2026-7615
Severity Medium (CVSS 4.3)
CWE 352
Vulnerable Version 1.3.3
Patched Version 1.4.0
Disclosed May 20, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-7615:
This vulnerability is a Cross-Site Request Forgery (CSRF) issue in the Widget Context plugin for WordPress, affecting all versions up to and including 1.3.3. The flaw allows an unauthenticated attacker to modify widget visibility context settings stored in the WordPress options table via a forged POST request to /wp-admin/widgets.php, provided the attacker can trick a site administrator into performing an action such as clicking on a link. The vulnerability carries a CVSS score of 4.3 (Medium).

Root Cause:
The root cause lies in the save_widget_context_settings() function at line 289 of widget-context/src/WidgetContext.php. In the vulnerable code, the function checks for user capabilities (edit_theme_options) and the presence of the ‘wl’ POST parameter but completely omits any nonce validation. The function processes $_POST[‘wl’] and directly updates $this->context_options without verifying that the request originated from the legitimate admin interface. The missing nonce validation means that any forged POST request that includes a valid ‘wl’ parameter can trigger settings modifications. The vulnerable code path is: save_widget_context_settings() -> array_merge of $_POST[‘wl’] into $this->context_options -> saving to the WordPress options table.

Exploitation:
An attacker can craft a malicious HTML form that is submitted to /wp-admin/widgets.php with a POST request. The form must include a ‘wl’ parameter containing an array of widget context settings the attacker wishes to impose. For example, an attacker could set visibility context for all widgets to ‘hide’, rendering them invisible across the site. The attack requires tricking an authenticated administrator into submitting this form (e.g., via a link click, image tag, or script injection in another site). Since WordPress does not require a nonce for widget context updates in the vulnerable version, the forged request is processed identically to a legitimate one. The specific endpoint is the standard WordPress widgets administration page (/wp-admin/widgets.php) which accepts POST data for widget context management.

Patch Analysis:
The patch introduces a nonce check before processing widget context updates. A new constant SAVE_NONCE_ACTION (line 18-22) is defined as ‘widget-context-update’. The save_widget_context_settings() function now generates a per-widget nonce action using get_widget_nonce_action() (line 766-769), which creates a unique nonce field name as ‘widget-context–‘ plus the widget ID. In the control_widget_context() function (line 740), a hidden nonce field is now output for each widget. The save function (line 293-304) now iterates over each widget ID in $_POST[‘wl’] and verifies the nonce using wp_verify_nonce() before processing the update or deletion. If the nonce fails, the widget’s context is not modified. This prevents forged requests from succeeding because the attacker would need to know the correct nonce value, which is session-specific and generated by WordPress for each legitimate admin user.

Impact:
Successful exploitation allows an attacker to arbitrarily change widget visibility context settings across the WordPress site. Widgets can be hidden from specific pages, shown only on certain URLs, or their display logic completely altered. This can lead to defacement, hiding critical site functionality, or exposing widgets that should be restricted. Since the plugin stores these settings in the WordPress options table, the attacker modifies persistent site configuration. The impact is limited to widget visibility context; the attacker cannot inject arbitrary content or escalate privileges directly, but the manipulated visibility could impair site usability, hide security-critical notices, or expose sensitive information.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/widget-context/src/WidgetContext.php
+++ b/widget-context/src/WidgetContext.php
@@ -15,6 +15,13 @@
 	 */
 	const RULE_KEY_URLS_INVERT = 'urls_invert';

+	/**
+	 * Nonce action when saving the individual widget context settings.
+	 *
+	 * @var string
+	 */
+	const SAVE_NONCE_ACTION = 'widget-context-update';
+
 	private $sidebars_widgets;
 	private $options_name = 'widget_logic_options'; // Context settings for widgets (visibility, etc)
 	private $settings_name = 'widget_context_settings'; // Widget Context global settings
@@ -96,9 +103,6 @@
 		// Register admin settings menu
 		add_action( 'admin_menu', array( $this, 'widget_context_settings_menu' ) );

-		// Register admin settings.
-		add_action( 'admin_init', array( $this, 'widget_context_settings_init' ) );
-
 		// Add quick links to the plugin list.
 		add_action(
 			'plugin_action_links_' . $this->plugin->basename(),
@@ -106,8 +110,9 @@
 		);
 	}

-
 	function define_widget_contexts() {
+		register_setting( $this->settings_name, $this->settings_name );
+
 		$this->context_options = apply_filters(
 			'widget_context_options',
 			(array) get_option( $this->options_name, array() )
@@ -161,6 +166,11 @@

 		// Sort contexts by their weight
 		uasort( $this->contexts, array( $this, 'sort_context_by_weight' ) );
+
+		if ( $this->is_legacy_widgets_enabled() ) {
+			add_filter( 'gutenberg_use_widgets_block_editor', '__return_false' );
+			add_filter( 'use_widgets_block_editor', '__return_false' );
+		}
 	}


@@ -279,29 +289,34 @@


 	function save_widget_context_settings() {
-		if ( ! current_user_can( 'edit_theme_options' ) || empty( $_POST ) || ! isset( $_POST['wl'] ) ) {
+		if ( ! current_user_can( 'edit_theme_options' ) || empty( $_POST['wl'] ) || ! is_array( $_POST['wl'] ) ) {
 			return;
 		}

-		// Delete a widget
-		if ( isset( $_POST['delete_widget'] ) && isset( $_POST['the-widget-id'] ) ) {
-			unset( $this->context_options[ $_POST['the-widget-id'] ] );
-		}
+		// Add and update.
+		foreach ( $_POST['wl'] as $widget_id => $widget_context_input ) {
+			$update_nonce = $this->get_widget_nonce_action( $widget_id );
+
+			if ( ! empty( $_POST[ $update_nonce ] ) && wp_verify_nonce( $_POST[ $update_nonce ], self::SAVE_NONCE_ACTION ) ) {
+				if ( ! isset( $this->context_options[ $widget_id ] ) ) {
+					$this->context_options[ $widget_id ] = array();
+				}

-		// Add / Update
-		$this->context_options = array_merge( $this->context_options, $_POST['wl'] );
+				if ( ! empty( $_POST['delete_widget'] ) ) { // Delete.
+					unset( $this->context_options[ $widget_id ] );
+				} else { // Update.
+					$this->context_options[ $widget_id ] = $widget_context_input;
+				}
+			}
+		}

-		$sidebars_widgets = wp_get_sidebars_widgets();
+		// Get a list of all widget IDs.
 		$all_widget_ids = array();
-
-		// Get a lits of all widget IDs
-		foreach ( $sidebars_widgets as $widget_area => $widgets ) {
-			foreach ( $widgets as $widget_order => $widget_id ) {
-				$all_widget_ids[] = $widget_id;
-			}
+		foreach ( wp_get_sidebars_widgets() as $widget_area => $widgets ) {
+			$all_widget_ids = array_merge( $all_widget_ids, array_values( $widgets ) );
 		}

-		// Remove non-existant widget contexts from the settings
+		// Cleanup non-existant widget contexts from the settings.
 		foreach ( $this->context_options as $widget_id => $widget_context ) {
 			if ( ! in_array( $widget_id, $all_widget_ids, true ) ) {
 				unset( $this->context_options[ $widget_id ] );
@@ -636,7 +651,6 @@
 		$controls_core = array();

 		foreach ( $this->contexts as $context_name => $context_settings ) {
-
 			$context_classes = array(
 				'context-group',
 				sprintf( 'context-group-%s', esc_attr( $context_name ) ),
@@ -726,6 +740,8 @@
 			}
 		}

+		$controls[] = wp_nonce_field( self::SAVE_NONCE_ACTION, $this->get_widget_nonce_action( $widget_id ), false, false );
+
 		return sprintf(
 			'<div class="widget-context">
 				<div class="widget-context-header">
@@ -746,6 +762,17 @@
 		);
 	}

+	/**
+	 * Get the nonce action for widget context settings.
+	 *
+	 * @param string $widget_id Widget ID.
+	 *
+	 * @return string
+	 */
+	private function get_widget_nonce_action( $widget_id ) {
+		return 'widget-context--' . $widget_id;
+	}
+

 	function control_incexc( $control_args ) {
 		$options = array(
@@ -1047,12 +1074,6 @@
 		);
 	}

-
-	function widget_context_settings_init() {
-		register_setting( $this->settings_name, $this->settings_name );
-	}
-
-
 	/**
 	 * Return a link to the Customize Widgets admin page.
 	 *
@@ -1072,6 +1093,15 @@
 		return admin_url( 'themes.php?page=widget_context_settings' );
 	}

+	/**
+	 * If the legacy widgets interface is enabled in the plugin settings.
+	 *
+	 * @return bool
+	 */
+	public function is_legacy_widgets_enabled() {
+		return ! empty( $this->context_settings['enable-legacy-widgets'] );
+	}
+

 	function widget_context_admin_view() {
 		$context_controls = array();
@@ -1140,6 +1170,21 @@
 									</p>
 								</td>
 							</tr>
+							<tr>
+								<th scrope="row">
+									<?php esc_html_e( 'Widget Interface', 'widget-context' ); ?>
+								</th>
+								<td>
+									<label>
+										<input type="hidden" name="<?php echo esc_attr( $this->settings_name ); ?>[enable-legacy-widgets]" value="0" />
+										<input type="checkbox" name="<?php echo esc_attr( $this->settings_name ); ?>[enable-legacy-widgets]" value="1" <?php checked( $this->context_settings['enable-legacy-widgets'], 1 ); ?> />
+										<?php esc_html_e( 'Enable legacy widget interface', 'widget-context' ); ?>
+									</label>
+									<p class="description">
+										<?php esc_html_e( 'Enable the legacy (non-block) widget interface under "Appearance → Widgets" that was disabled in WordPress 5.8.', 'widget-context' ); ?>
+									</p>
+								</td>
+							</tr>
 							<tr>
 								<th scrope="row">
 									<?php esc_html_e( 'Configure Widgets', 'widget-context' ); ?>
--- a/widget-context/vendor/autoload.php
+++ b/widget-context/vendor/autoload.php
@@ -14,10 +14,7 @@
             echo $err;
         }
     }
-    trigger_error(
-        $err,
-        E_USER_ERROR
-    );
+    throw new RuntimeException($err);
 }

 require_once __DIR__ . '/composer/autoload_real.php';
--- a/widget-context/vendor/composer/InstalledVersions.php
+++ b/widget-context/vendor/composer/InstalledVersions.php
@@ -27,12 +27,23 @@
 class InstalledVersions
 {
     /**
+     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+     * @internal
+     */
+    private static $selfDir = null;
+
+    /**
      * @var mixed[]|null
      * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
      */
     private static $installed;

     /**
+     * @var bool
+     */
+    private static $installedIsLocalDir;
+
+    /**
      * @var bool|null
      */
     private static $canGetVendors;
@@ -309,6 +320,24 @@
     {
         self::$installed = $data;
         self::$installedByVendor = array();
+
+        // when using reload, we disable the duplicate protection to ensure that self::$installed data is
+        // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
+        // so we have to assume it does not, and that may result in duplicate data being returned when listing
+        // all installed packages for example
+        self::$installedIsLocalDir = false;
+    }
+
+    /**
+     * @return string
+     */
+    private static function getSelfDir()
+    {
+        if (self::$selfDir === null) {
+            self::$selfDir = strtr(__DIR__, '\', '/');
+        }
+
+        return self::$selfDir;
     }

     /**
@@ -322,19 +351,27 @@
         }

         $installed = array();
+        $copiedLocalDir = false;

         if (self::$canGetVendors) {
+            $selfDir = self::getSelfDir();
             foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+                $vendorDir = strtr($vendorDir, '\', '/');
                 if (isset(self::$installedByVendor[$vendorDir])) {
                     $installed[] = self::$installedByVendor[$vendorDir];
                 } elseif (is_file($vendorDir.'/composer/installed.php')) {
                     /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
                     $required = require $vendorDir.'/composer/installed.php';
-                    $installed[] = self::$installedByVendor[$vendorDir] = $required;
-                    if (null === self::$installed && strtr($vendorDir.'/composer', '\', '/') === strtr(__DIR__, '\', '/')) {
-                        self::$installed = $installed[count($installed) - 1];
+                    self::$installedByVendor[$vendorDir] = $required;
+                    $installed[] = $required;
+                    if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+                        self::$installed = $required;
+                        self::$installedIsLocalDir = true;
                     }
                 }
+                if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
+                    $copiedLocalDir = true;
+                }
             }
         }

@@ -350,7 +387,7 @@
             }
         }

-        if (self::$installed !== array()) {
+        if (self::$installed !== array() && !$copiedLocalDir) {
             $installed[] = self::$installed;
         }

--- a/widget-context/vendor/composer/installed.php
+++ b/widget-context/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'kasparsd/widget-context',
-        'pretty_version' => 'dev-master',
-        'version' => 'dev-master',
-        'reference' => 'a2e25bade0a8c6117d67462fa1df8ec6c19f2b31',
+        'pretty_version' => 'dev-develop',
+        'version' => 'dev-develop',
+        'reference' => '1332f3f9829bd0866867e6b1401a508bca6f9ea2',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -11,9 +11,9 @@
     ),
     'versions' => array(
         'kasparsd/widget-context' => array(
-            'pretty_version' => 'dev-master',
-            'version' => 'dev-master',
-            'reference' => 'a2e25bade0a8c6117d67462fa1df8ec6c19f2b31',
+            'pretty_version' => 'dev-develop',
+            'version' => 'dev-develop',
+            'reference' => '1332f3f9829bd0866867e6b1401a508bca6f9ea2',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
--- a/widget-context/widget-context.php
+++ b/widget-context/widget-context.php
@@ -3,7 +3,7 @@
  * Plugin Name: Widget Context
  * Plugin URI: https://widgetcontext.com
  * Description: Show or hide widgets depending on the section of the site that is being viewed. Configure the widget visibility rules under the individual widget settings.
- * Version: 1.3.3
+ * Version: 1.4.0
  * Author: Kaspars Dambis
  * Author URI: https://widgetcontext.com
  * Text Domain: widget-context

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School