VYPR
Medium severity4.3NVD Advisory· Published May 22, 2026

CVE-2026-7615

CVE-2026-7615

Description

The Widget Context plugin for WordPress is vulnerable to Cross-Site Request Forgery in all versions up to, and including, 1.3.3. This is due to missing or incorrect nonce validation on the save_widget_context_settings function. This makes it possible for unauthenticated attackers to modify widget visibility context settings stored in the WordPress options table via a forged POST request to /wp-admin/widgets.php via a forged request granted they can trick a site administrator into performing an action such as clicking on a link.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CSRF in Widget Context plugin ≤1.3.3 allows unauthenticated attackers to modify widget visibility settings via forged request.

Vulnerability

The Widget Context plugin for WordPress versions up to and including 1.3.3 is vulnerable to Cross-Site Request Forgery (CSRF) due to missing or incorrect nonce validation on the save_widget_context_settings function [1]. This allows attackers to forge POST requests to /wp-admin/widgets.php.

Exploitation

An unauthenticated attacker can exploit this CSRF by tricking a site administrator into performing an action such as clicking on a link. The attacker does not need any authentication or special privileges, only the ability to craft a malicious request and deliver it to the administrator.

Impact

Successful exploitation enables the attacker to modify widget visibility context settings stored in the WordPress options table. This could lead to unintended display or hiding of widgets, potentially altering site appearance or functionality. The impact is limited to these settings and does not result in direct data breach or code execution.

Mitigation

The fix has been implemented in commit ve on Pull Request #73 [1] and is expected to be included in version 1.3.4 or later. Users should update the plugin as soon as the fixed version is available. Until then, administrators should avoid clicking on untrusted links and ensure they have strong administrative practices in place.

AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
21e51adaecc9

Validate admin actions and add legacy widget UI toggle (#73)

https://github.com/kasparsd/widget-context-wporgKaspars DambisMay 18, 2026via nvd-ref
12 files changed · +2392 1354
  • composer.json+10 11 modified
    @@ -19,23 +19,22 @@
       "config": {
         "sort-packages": true,
         "platform": {
    -      "php": "5.6.20"
    +      "php": "7.4"
         },
         "allow-plugins": {
           "dealerdirect/phpcodesniffer-composer-installer": true
         }
       },
       "require-dev": {
    -    "10up/wp_mock": "^0.2.0",
    -    "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
    -    "mockery/mockery": "^0.9.5",
    -    "php-coveralls/php-coveralls": "^2.4",
    -    "phpcompatibility/php-compatibility": "dev-develop as 9.99.99",
    -    "phpcompatibility/phpcompatibility-wp": "^2.1",
    -    "phpmd/phpmd": "^2.10",
    -    "phpunit/phpunit": "^5.7",
    -    "wp-coding-standards/wpcs": "^3.1",
    -    "yoast/phpunit-polyfills": "^2.0"
    +    "10up/wp_mock": "^1.1",
    +    "dealerdirect/phpcodesniffer-composer-installer": "^1.2",
    +    "mockery/mockery": "^1.6",
    +    "php-coveralls/php-coveralls": "^2.9",
    +    "phpcompatibility/phpcompatibility-wp": "^3.0@alpha",
    +    "phpmd/phpmd": "^2.15",
    +    "phpunit/phpunit": "^9.6",
    +    "wp-coding-standards/wpcs": "^3.3",
    +    "yoast/phpunit-polyfills": "^4.0"
       },
       "autoload": {
         "classmap": [
    
  • composer.lock+2114 1280 modified
  • docker-compose.yml+1 17 modified
    @@ -1,5 +1,3 @@
    -version: '3'
    -
     services:
     
       mysql:
    @@ -14,7 +12,7 @@ services:
           MYSQL_ROOT_PASSWORD: password
     
       wordpress:
    -    image: wordpress:php7.4
    +    image: wordpress:php8.4
         depends_on:
           - mysql
         ports:
    @@ -44,20 +42,6 @@ services:
           WORDPRESS_DB_USER: wordpress
           WORDPRESS_DB_PASSWORD: password
     
    -  wp-cli-php8:
    -    image: wordpress:cli-php8.2
    -    depends_on:
    -      - mysql
    -    volumes:
    -      - wp_data:/var/www/html
    -      - .:/var/www/html/wp-content/plugins/widget-context-src
    -      - ./dist:/var/www/html/wp-content/plugins/widget-context
    -    restart: always
    -    environment:
    -      WORDPRESS_DEBUG: 1
    -      WORDPRESS_DB_USER: wordpress
    -      WORDPRESS_DB_PASSWORD: password
    -
     volumes:
       db_data: {}
       wp_data: {}
    
  • .github/workflows/test.yml+12 5 modified
    @@ -6,18 +6,25 @@ jobs:
     
       test:
         name: Test and Lint
    -    runs-on: ubuntu-20.04
    +    runs-on: ubuntu-24.04
         strategy:
           matrix:
    -        php-versions: ['5.6', '7.4', '8.0']
    +        php-versions:
    +          - 7.4
    +          - 8.0
    +          - 8.1
    +          - 8.2
    +          - 8.3
    +          - 8.4
         steps:
           - name: Checkout
    -        uses: actions/checkout@v2
    +        uses: actions/checkout@v6
     
           - name: Setup Node
    -        uses: actions/setup-node@v2
    +        uses: actions/setup-node@v6
             with:
    -          node-version: '14'
    +          cache: npm
    +          node-version-file: .nvmrc
     
           - name: Setup PHP
             uses: shivammathur/setup-php@v2
    
  • .gitignore+1 0 modified
    @@ -5,3 +5,4 @@
     /phpcs.xml
     /phpunit.xml
     .DS_Store
    +.phpunit.result.cache
    
  • phpcs.xml.dist+1 1 modified
    @@ -10,7 +10,7 @@
     	<file>.</file>
     
     	<rule ref="PHPCompatibility" />
    -	<config name="testVersion" value="5.6-" />
    +	<config name="testVersion" value="7.4-" />
     
     	<rule ref="WordPress-Core">
     		<exclude name="WordPress.Files.FileName" /><!-- We'll be switching to PSR-4 naming soon. -->
    
  • readme.txt.md+8 2 modified
    @@ -3,10 +3,10 @@
     Contributors: kasparsd, jamescollins  
     Tags: widget, widgets, widget context, context, logic, widget logic, visibility, widget visibility  
     Requires at least: 3.0  
    -Tested up to: 6.6  
    +Tested up to: 6.9  
     Stable tag: {{ version }}  
     License: GPLv2 or later  
    -Requires PHP: 5.6  
    +Requires PHP: 7.4  
     Donate link: https://widgetcontext.com/pro
     
     Show and hide widgets on specific posts, pages and sections of your site.
    @@ -65,6 +65,12 @@ Specify URLs to ignore even if they're matched by any of the other context rules
     
     ## Changelog
     
    +### 1.4.0 (May 18, 2026)
    +
    +- Security: Ensure widget context settings can only be changed by logged-in administrators intentionally (CVE-2026-7615).
    +- Require PHP 7.4 to match the WordPress core requirements.
    +- Feature: Add a toggle to enable the legacy widget interface (non-block).
    +
     ### 1.3.3 (August 31, 2024)
     
     - Mark as tested with WordPress 6.6 and PHP 8.2, see [#72](https://github.com/kasparsd/widget-context-wporg/pull/72).
    
  • src/WidgetContext.php+71 26 modified
    @@ -15,6 +15,13 @@ class WidgetContext {
     	 */
     	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,18 +103,16 @@ public function init() {
     		// 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(),
     			array( $this, 'plugin_action_links' )
     		);
     	}
     
    -
     	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 @@ function define_widget_contexts() {
     
     		// 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 widget_context_controls( $widget ) {
     
     
     	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 / Update
    -		$this->context_options = array_merge( $this->context_options, $_POST['wl'] );
    +		// Add and update.
    +		foreach ( $_POST['wl'] as $widget_id => $widget_context_input ) {
    +			$update_nonce = $this->get_widget_nonce_action( $widget_id );
     
    -		$sidebars_widgets = wp_get_sidebars_widgets();
    -		$all_widget_ids = array();
    +			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();
    +				}
     
    -		// 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;
    +				if ( ! empty( $_POST['delete_widget'] ) ) { // Delete.
    +					unset( $this->context_options[ $widget_id ] );
    +				} else { // Update.
    +					$this->context_options[ $widget_id ] = $widget_context_input;
    +				}
     			}
     		}
     
    -		// Remove non-existant widget contexts from the settings
    +		// Get a list of all widget IDs.
    +		$all_widget_ids = array();
    +		foreach ( wp_get_sidebars_widgets() as $widget_area => $widgets ) {
    +			$all_widget_ids = array_merge( $all_widget_ids, array_values( $widgets ) );
    +		}
    +
    +		// 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 @@ function display_widget_context( $widget_id = null ) {
     		$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 @@ function display_widget_context( $widget_id = null ) {
     			}
     		}
     
    +		$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 @@ function display_widget_context( $widget_id = null ) {
     		);
     	}
     
    +	/**
    +	 * 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_menu() {
     		);
     	}
     
    -
    -	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 @@ public function plugin_settings_admin_url() {
     		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 @@ function widget_context_admin_view() {
     									</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' ); ?>
    
  • tests/php/bootstrap.php+0 9 modified
    @@ -4,12 +4,3 @@
      */
     
     WP_Mock::bootstrap();
    -
    -/**
    - * Patch the following error in PHP 8:
    - * Uncaught Error: Class "PHP_Token_NAME_QUALIFIED" not found
    - * in vendor/phpunit/php-token-stream/src/Token/Stream.php:189
    - */
    -if ( ! class_exists( 'PHP_Token_NAME_QUALIFIED' ) ) {
    -	class PHP_Token_NAME_QUALIFIED extends PHP_Token {}
    -}
    
  • tests/php/WidgetContextTargetByUrlTest.php+1 1 modified
    @@ -30,7 +30,7 @@ class WidgetContextTargetByUrlTest extends WidgetContextTestCase {
     		'/page/?query=string' => 'page?query=string',
     	);
     
    -	public function setUp() {
    +	public function setUp(): void {
     		parent::setUp();
     
     		$this->plugin = new \WidgetContext( null );
    
  • tests/php/WidgetContextTest.php+172 1 modified
    @@ -7,9 +7,15 @@
     
     class WidgetContextTest extends WidgetContextTestCase {
     
    -	public function setUp() {
    +	protected $plugin;
    +
    +	private $post_backup;
    +
    +	public function setUp(): void {
     		parent::setUp();
     
    +		$this->post_backup = $_POST;
    +
     		$this->plugin = new \WidgetContext( null );
     
     		WP_Mock::userFunction( 'wp_parse_args' )
    @@ -22,6 +28,12 @@ function ( $args, $defaults ) {
     		WP_Mock::alias( 'wp_parse_url', 'parse_url' );
     	}
     
    +	public function tearDown(): void {
    +		$_POST = $this->post_backup;
    +
    +		parent::tearDown();
    +	}
    +
     	public function testLegacyInstance() {
     		$widget_context = new WidgetContext( null );
     
    @@ -68,4 +80,163 @@ public function testRequestPathResolver() {
     			'Normalize the path by removing the trailing slash'
     		);
     	}
    +
    +	public function testSaveWidgetContextSettingsUpdatesValidWidgetContextsAndCleansStaleOptions() {
    +		$this->setContextOptions(
    +			array(
    +				'text-2'     => array(
    +					'incexc' => array(
    +						'condition' => 'hide',
    +					),
    +				),
    +				'archives-3' => array(
    +					'incexc' => array(
    +						'condition' => 'show',
    +					),
    +				),
    +			)
    +		);
    +
    +		$_POST = array(
    +			'wl'                      => array(
    +				'text-2'        => array(
    +					'incexc' => array(
    +						'condition' => 'selected',
    +					),
    +					'url'    => array(
    +						'paths' => 'news/*',
    +					),
    +				),
    +				'custom_html-4' => array(
    +					'incexc' => array(
    +						'condition' => 'notselected',
    +					),
    +				),
    +			),
    +			'widget-context--text-2' => 'valid-nonce',
    +		);
    +
    +		$expected_options = array(
    +			'text-2' => array(
    +				'incexc' => array(
    +					'condition' => 'selected',
    +				),
    +				'url'    => array(
    +					'paths' => 'news/*',
    +				),
    +			),
    +		);
    +
    +		WP_Mock::userFunction(
    +			'current_user_can',
    +			array(
    +				'args'   => array( 'edit_theme_options' ),
    +				'times'  => 1,
    +				'return' => true,
    +			)
    +		);
    +
    +		WP_Mock::userFunction( 'wp_verify_nonce' )
    +			->andReturnUsing(
    +				function ( $nonce, $action ) {
    +					return 'valid-nonce' === $nonce && \WidgetContext::SAVE_NONCE_ACTION === $action;
    +				}
    +			);
    +
    +		WP_Mock::userFunction(
    +			'wp_get_sidebars_widgets',
    +			array(
    +				'times'  => 1,
    +				'return' => array(
    +					'sidebar-1' => array( 'text-2', 'custom_html-4' ),
    +				),
    +			)
    +		);
    +
    +		WP_Mock::userFunction(
    +			'update_option',
    +			array(
    +				'args'  => array( 'widget_logic_options', $expected_options ),
    +				'times' => 1,
    +			)
    +		);
    +
    +		$this->plugin->save_widget_context_settings();
    +
    +		$this->assertSame( $expected_options, $this->plugin->get_context_options() );
    +	}
    +
    +	public function testSaveWidgetContextSettingsDeletesWidgetContext() {
    +		$this->setContextOptions(
    +			array(
    +				'text-2' => array(
    +					'incexc' => array(
    +						'condition' => 'selected',
    +					),
    +				),
    +			)
    +		);
    +
    +		$_POST = array(
    +			'delete_widget'           => 1,
    +			'wl'                      => array(
    +				'text-2' => array(
    +					'incexc' => array(
    +						'condition' => 'selected',
    +					),
    +				),
    +			),
    +			'widget-context--text-2' => 'valid-nonce',
    +		);
    +
    +		WP_Mock::userFunction(
    +			'current_user_can',
    +			array(
    +				'args'   => array( 'edit_theme_options' ),
    +				'times'  => 1,
    +				'return' => true,
    +			)
    +		);
    +
    +		WP_Mock::userFunction(
    +			'wp_verify_nonce',
    +			array(
    +				'args'   => array( 'valid-nonce', \WidgetContext::SAVE_NONCE_ACTION ),
    +				'times'  => 1,
    +				'return' => true,
    +			)
    +		);
    +
    +		WP_Mock::userFunction(
    +			'wp_get_sidebars_widgets',
    +			array(
    +				'times'  => 1,
    +				'return' => array(
    +					'sidebar-1' => array( 'text-2' ),
    +				),
    +			)
    +		);
    +
    +		WP_Mock::userFunction(
    +			'update_option',
    +			array(
    +				'args'  => array( 'widget_logic_options', array() ),
    +				'times' => 1,
    +			)
    +		);
    +
    +		$this->plugin->save_widget_context_settings();
    +
    +		$this->assertSame( array(), $this->plugin->get_context_options() );
    +	}
    +
    +	private function setContextOptions( $context_options ) {
    +		$property = new \ReflectionProperty( \WidgetContext::class, 'context_options' );
    +
    +		if ( PHP_VERSION_ID < 80100 ) {
    +			$property->setAccessible( true );
    +		}
    +
    +		$property->setValue( $this->plugin, $context_options );
    +	}
     }
    
  • widget-context.php+1 1 modified
    @@ -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
    

Vulnerability mechanics

Root cause

"Missing or incorrect nonce validation on the save_widget_context_settings function allows Cross-Site Request Forgery."

Attack vector

An unauthenticated attacker crafts a malicious POST request to /wp-admin/widgets.php containing forged widget visibility context settings. The attacker must trick a logged-in site administrator into submitting this request, for example by clicking a link or visiting a crafted page. Because the save_widget_context_settings function lacks proper nonce validation [patch_id=1578913], the request is processed as if it came from the administrator, allowing the attacker to modify widget visibility context options stored in the WordPress options table.

Affected code

The vulnerability exists in the save_widget_context_settings function, which handles POST requests to /wp-admin/widgets.php for updating widget visibility context options. The patch [patch_id=1578913] modifies this function to add nonce verification. The advisory does not specify the exact file path or line numbers.

What the fix does

The patch adds a nonce check using wp_verify_nonce() inside the save_widget_context_settings function [patch_id=1578913]. This ensures that the POST request originated from the legitimate WordPress admin interface and was intentionally submitted by the administrator. Without this check, any forged request could modify widget visibility settings; with the nonce validation in place, only requests carrying a valid, session-bound nonce token are accepted.

Preconditions

  • authA site administrator must be logged into WordPress when the forged request is submitted.
  • inputAttacker must craft a POST request to /wp-admin/widgets.php with modified widget visibility context parameters.
  • networkAttacker must deliver the forged request to the administrator (e.g., via a link or embedded form on another site).

Generated on May 22, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.