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(expand)+ 1 more
- (no CPE)
- (no CPE)range: <=1.3.3
Patches
121e51adaecc9Validate admin actions and add legacy widget UI toggle (#73)
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 modifieddocker-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- github.com/kasparsd/widget-context-wporg/pull/73nvd
- plugins.trac.wordpress.org/browser/widget-context/tags/1.3.3/src/WidgetContext.phpnvd
- plugins.trac.wordpress.org/browser/widget-context/tags/1.3.3/src/WidgetContext.phpnvd
- plugins.trac.wordpress.org/browser/widget-context/tags/1.3.3/src/WidgetContext.phpnvd
- plugins.trac.wordpress.org/browser/widget-context/trunk/src/WidgetContext.phpnvd
- plugins.trac.wordpress.org/browser/widget-context/trunk/src/WidgetContext.phpnvd
- plugins.trac.wordpress.org/browser/widget-context/trunk/src/WidgetContext.phpnvd
- www.wordfence.com/threat-intel/vulnerabilities/id/3c434637-4bf9-46ee-9a6d-35eab7ef11a1nvd
News mentions
0No linked articles in our index yet.