CVE case study
CVE-2026-59551: rtMedia Validated the ORDER BY Prefix but Executed the Full Value
rtMedia checked only the leading sort column and direction, then interpolated the complete order_by value into SQL. A subscriber could append an expression and create a blind SQL oracle. Version 4.7.11 rebuilds the clause exclusively from validated tokens.
- Severity
- High (8.5)
- Scoring
- CVSS 3.1
- Weakness
- CWE-89
- Affected
- rtMedia 4.7.10 and earlier
- Remediation state
- Upgrade to rtMedia 4.7.11 or later
- Advisory published
- 23 Jul 2026
Official vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:L
Why it matters
Sorting looks like a small feature, but an SQL ORDER BY clause is executable query structure. rtMedia accepted a caller-controlled order_by value, checked its first two space-delimited tokens, and then reused the complete original string when it built the database query.
That difference between what the code validated and what it executed created a blind SQL injection primitive. A subscriber with a valid rtMedia API token could begin the value with an allowed column and direction, then place an additional expression after that accepted prefix.
The official CVE score is High at 8.5, with high confidentiality impact, low availability impact, and no integrity impact. My validation stopped after confirming a repeatable blind oracle and one known byte in synthetic local fixture data. I did not test a third-party site, modify database records, or attempt complete extraction.
How I found it
I found this while reviewing the earlier order_by hardening associated with CVE-2026-15287. The validation still present in rtMedia 4.7.10 looked like an allowlist, but I followed both the parsed tokens and the original string to see which representation reached the database.
The check split order_by on spaces and validated only the first two values as a column and direction. If those values were accepted, the original string remained unchanged. The code then escaped and interpolated that full string into the ORDER BY clause.
A normal sort and a value with an accepted two-token prefix therefore took the same validation branch, even though the latter contained additional content. That was the key mismatch: the decision applied to two tokens, while the sink consumed every token.
I traced the reachable source through rtmedia_api_process_rtmedia_gallery_request(). The handler verifies an rtMedia API token, reads order_by from the request, applies text sanitization, and passes it to RTMediaModel::get(). Neither text sanitization nor esc_sql() makes a caller-selected SQL expression safe.
I validated the blind behavior in an owned WordPress and MariaDB lab with BuddyPress, rtMedia's JSON API, a synthetic subscriber, and synthetic media data. Paired controls produced a stable baseline, true-condition delay, and false-condition response. One known byte in local fixture data confirmed the oracle, then I stopped. No production target or real user record was involved.
I reported the issue on 16 July 2026. rtMedia 4.7.11 was released on 20 July with stricter sorting validation. Patchstack published the verified record on 23 July, and the CVE Program record was published on 27 July.
Root cause
The rtMedia gallery API verified its token, read order_by through WordPress text sanitization, and passed the result to RTMediaModel::get(). Text sanitization does not turn a caller-selected SQL identifier or expression into trusted query structure.
Version 4.7.10 split the value on spaces and assigned only the first two tokens to $order_column and $order_direction. It compared those tokens with three allowed columns and the accepted directions. If they passed, the code left $order_by unchanged.
The query builder then applied esc_sql() to that unchanged value and interpolated it after ORDER BY. Escaping data is not a substitute for selecting SQL grammar from fixed values. Any trailing content after a valid prefix survived the allowlist decision.
Version 4.7.11 fixes the representation mismatch. It validates the parsed column and direction with strict comparisons, replaces invalid values with a safe default, and reconstructs $order_by from only those two canonical tokens. The original request string is no longer reused.
Source-to-sink trace
- 01Authenticated API boundary
rtmedia_api_process_rtmedia_gallery_request()The gallery method verifies an rtMedia API token. The accepted public classification sets the attacker floor at Subscriber.
- 02Request-controlled sort
RTMediaJsonApi.php:1388 in 4.7.10The handler reads order_by through FILTER_SANITIZE_FULL_SPECIAL_CHARS and sanitize_text_field(). Those functions sanitize text, not SQL grammar.
- 03Model handoff
RTMediaJsonApi.php:1396 in 4.7.10The complete order_by value is passed as the fourth argument to RTMediaModel::get().
- 04Partial allowlist
RTMediaModel.php:124-136 in 4.7.10Only the first space-delimited column and direction are checked. A valid pair leaves the original value unchanged, including any trailing tokens.
- 05SQL construction
RTMediaModel.php:138-140 in 4.7.10The full original value is passed through esc_sql() and interpolated after ORDER BY. Escaping does not reduce it to the two validated tokens.
- 06Database execution
RTMediaModel.php:153-176 in 4.7.10The assembled query reaches $wpdb->get_results() or $wpdb->get_var(), creating a blind query-influence primitive.
- 07Canonical reconstruction
RTMediaModel.php in 4.7.11The fixed code validates both tokens strictly and rebuilds order_by from only the accepted column and direction, discarding the trailing string.
Safe proof of concept
Prerequisites
- Python 3, curl, and diff.
- Network access only to download the public rtMedia 4.7.10 and 4.7.11 model files from the WordPress plugin repository.
- No WordPress account, API token, database, or running website. All marker values remain local strings.
Step-by-step reproduction
- Download the public
RTMediaModel.phpfiles from rtMedia 4.7.10 and 4.7.11. - Compare the sorting block. Confirm that 4.7.10 validates two parsed tokens but later uses the complete original
$order_byvalue. - Confirm that 4.7.11 adds strict comparisons and reconstructs
$order_byfrom$order_columnand$order_direction. - Run the inert Python model below with a valid column, valid direction, and the non-SQL marker
SYNTHETIC_TRAILING_EXPRESSION. - Confirm that the affected model retains the trailing marker while the fixed model returns only
media_id asc. - Run the normal-sort and invalid-token controls. A legitimate sort should remain stable, while invalid input should fall back to
media_id descin both models.
Compare the public affected and fixed sorting paths
LAB_DIR=$(mktemp -d "\${TMPDIR:-/tmp}/rtmedia-cve.XXXXXX")
SOURCE_PATH=app/helper/RTMediaModel.php
for VERSION in 4.7.10 4.7.11; do
curl -fsSL \
"https://plugins.svn.wordpress.org/buddypress-media/tags/$VERSION/$SOURCE_PATH" \
-o "$LAB_DIR/rtmedia-model-$VERSION.php"
done
diff -u \
<(sed -n '124,145p' "$LAB_DIR/rtmedia-model-4.7.10.php") \
<(sed -n '124,150p' "$LAB_DIR/rtmedia-model-4.7.11.php")Inert allowlist decision model
ALLOWED_COLUMNS = {"media_id", "media_title", "file_size"}
ALLOWED_DIRECTIONS = {"asc", "desc", ""}
def first_two(raw):
parts = (raw + " ").split(" ")
return parts[0], parts[1]
def affected(raw):
column, direction = first_two(raw)
if column.lower() not in ALLOWED_COLUMNS:
return "media_id desc"
if direction.lower() not in ALLOWED_DIRECTIONS:
return "media_id desc"
return raw
def fixed(raw):
column, direction = first_two(raw)
if (
column.lower() not in ALLOWED_COLUMNS
or direction.lower() not in ALLOWED_DIRECTIONS
):
column, direction = "media_id", "desc"
return f"{column} {direction}".strip()
cases = {
"trailing marker": "media_id asc SYNTHETIC_TRAILING_EXPRESSION",
"normal sort": "media_title desc",
"invalid column": "custom_field asc SYNTHETIC_TRAILING_EXPRESSION",
"invalid direction": "media_id sideways SYNTHETIC_TRAILING_EXPRESSION",
}
for label, value in cases.items():
print(label)
print(" 4.7.10:", affected(value))
print(" 4.7.11:", fixed(value))Expected inert result
trailing marker
4.7.10: media_id asc SYNTHETIC_TRAILING_EXPRESSION
4.7.11: media_id asc
normal sort
4.7.10: media_title desc
4.7.11: media_title desc
invalid column
4.7.10: media_id desc
4.7.11: media_id desc
invalid direction
4.7.10: media_id desc
4.7.11: media_id descExpected evidence
- The public source diff shows that 4.7.10 checks only the leading column and direction while retaining the complete original value.
- The inert trailing marker survives the affected model because its first two tokens are allowed.
- Version 4.7.11 returns only the validated
media_id ascpair and discards the marker. - The normal-sort control is unchanged, showing that canonical reconstruction preserves intended behavior.
- Invalid columns and directions fall back to
media_id descin both models.
Use media_title desc as the valid control and invalid column or direction values as rejection controls. The fixed model must preserve the valid pair, apply the default to invalid input, and never carry a third token into the query fragment.
Review or install rtMedia 4.7.11 or later in an owned test environment. Confirm that the sort clause is constructed only from the allowlisted column and direction, an inert trailing marker is discarded, invalid input falls back safely, and each documented sort still returns the expected order.
Impact
The accepted public classification requires an authenticated Subscriber. The reachable path also depends on BuddyPress, the rtMedia JSON API being enabled, and a valid rtMedia API token. Those requirements should be checked before treating an installation as exposed.
A successful attacker could influence database evaluation through a boolean or timing oracle and infer confidential values incrementally. In the owned lab, paired controls produced a repeatable difference and a single known fixture byte was inferred before testing stopped.
The evidence does not establish remote code execution, database modification, administrator takeover, or guaranteed access to every table. The official vector records no integrity impact. It also does not mean that every WordPress site or every rtMedia installation exposed the vulnerable configuration.
Fix and retest
Upgrade rtMedia to 4.7.11 or later. The 4.7.11 changelog describes improved validation for sorting parameters, and the source reconstructs the sort clause only from the validated column and direction.
For any dynamic SQL ordering feature, map a small set of public sort names to hardcoded database identifiers and accept direction from a fixed ASC or DESC set. Do not pass an original request fragment forward after checking only part of it.
Retest with a normal allowed sort, an invalid column, an invalid direction, and an inert trailing marker. The fixed version should preserve the normal sort, fall back safely for invalid inputs, and discard everything after the validated pair.
Patchstack mitigation can reduce exposure while an update is scheduled, but it is an interim control. Removing the unsafe query construction by upgrading remains the durable repair.
Engineering lesson
An allowlist is effective only when the program executes the exact value it validated. Validating a prefix and consuming the full string leaves an unreviewed suffix inside the trust boundary.
SQL identifiers and sort directions are grammar, not ordinary bound data. The safe pattern is to parse, validate, canonicalize, and construct the fragment from known constants.
Patch review should follow values beyond the new check. A validation block can look convincing in isolation while the downstream sink still receives the pre-validation representation.
References
- Patchstack CVE-2026-59551 record and researcher credit opens in a new tab
- Central CVE-2026-59551 record opens in a new tab
- rtMedia plugin page and 4.7.11 release changelog opens in a new tab
- Affected RTMediaModel source in 4.7.10 opens in a new tab
- Affected gallery API source in 4.7.10 opens in a new tab
- Fixed RTMediaModel source in 4.7.11 opens in a new tab
- WordPress SVN changeset 3616500 opens in a new tab
- Earlier order_by record CVE-2026-15287 opens in a new tab