SECURITY ADVISORY / 01

CVE-2025-13673 Exploit & Vulnerability Analysis

Complete CVE-2025-13673 security advisory with proof of concept (PoC), exploit details, and patch analysis for tutor.

tutor products NVD ↗
Exploit PoC Vulnerability Patch Analysis

The Exploit

An unauthenticated attacker can inject arbitrary SQL into coupon lookups by appending SQL metacharacters to the coupon_code parameter.

GET /wp-admin/admin-ajax.php?action=tutor_get_coupon_details&coupon_code=VALID' UNION SELECT user_login, user_pass, 3, 4, 5 FROM wp_users WHERE '1'='1 HTTP/1.1
Host: target.local

The attacker observes a JSON response containing password hashes from the wp_users table. If no error suppression is in place, they see UNION query results merged into the coupon details response; if errors are suppressed, they can use time-based blind injection (SLEEP()) to infer data byte-by-byte.

What the Patch Did

Before

'coupon_code'   => esc_sql( $coupon_code ),

After

'coupon_code'   => $coupon_code,

Wait — this looks backwards. The patch removes esc_sql(). Let's look at the real fix in QueryHelper.php:

Before

default: // =, !=, <, >, <=, >=, LIKE, NOT LIKE, <>
    $val    = is_numeric( $val ) ? $val : "'" . $val . "'";
    $clause = array( $field, $operator, $val );
    break;

After

else {
    $value = self::prepare_value( $val );
}

return "{$field} {$upper_operator} {$value}";

The patch introduces a new prepare_value() method that wraps all scalar values in wpdb->prepare('%s', $value) or wpdb->prepare('%d', $value) depending on type. This shifts the escaping responsibility from esc_sql() (string-only, insufficient) to parameterized queries using WordPress's $wpdb->prepare(). The removal of esc_sql() from CouponModel.php is intentional — it signals that the value will be processed through the query builder, not directly concatenated.

Root Cause

CWE-89: Improper Neutralization of Special Elements used in an SQL Command.

The coupon_code parameter arrives via HTTP request (typically $_GET['coupon_code'] or REST API input) and flows into CouponModel::get_coupon(). Before the patch, this method passed the coupon code to QueryHelper::build_where_clause(), which constructed SQL fragments by string concatenation. For operators other than IN and NOT IN, the code checked is_numeric() and then wrapped the value in single quotes without escaping, leaving SQL metacharacters like ', --, /*, and UNION unescaped. A malicious value like VALID' UNION SELECT ... would break out of the intended query and execute arbitrary SQL.

Why It Works

The load-bearing line is $value = self::prepare_value( $val ). This call routes the value through wpdb->prepare(), which uses parameterized queries (prepared statements in MySQL parlance) to separate code from data. The old code tried to "fix" the problem with is_numeric() and manual quoting — these additions are security theater. is_numeric('1e5') returns true, but '1e5' as a number can trigger scientific notation in some contexts. There is no safe way to concatenate user input into SQL by hand. The patch removes that impossible task entirely, delegating it to the database driver.

The additional cases for BETWEEN and NULL operators are load-bearing too: they ensure that all code paths through build_where_clause() use prepare_value(), closing loopholes where an attacker could choose an unusual operator to bypass escaping.

Hardening Checklist

  • Use wpdb->prepare() for all variable interpolation — never concatenate user input into SQL, even if you think you've "escaped" it with esc_sql(). Pass a format string ('%s', '%d', '%i') and let the driver parameterize.

  • Audit all QueryBuilder-like classes for string concatenation — grep your codebase for return "{$field}" or array( $field, ... ) patterns that construct SQL. Each must pass through $wpdb->prepare() before database execution.

  • Validate operator whitelist in switch statements — if your code accepts an operator parameter, use a strict whitelist (IN, =, LIKE, etc.) and reject unrecognized values. The patch implicitly does this by handling BETWEEN in its own case; unhandled operators fall through to the else block.

  • Test with time-based blind SQL injection payloads — include ' OR SLEEP(5)-- and ' AND SLEEP(5)-- in your unit test suite for any parameter that flows to the database. If response time increases, your escaping is bypassed.

  • Never rely on is_numeric() for SQL type detection — it is a type check, not a sanitizer. Use explicit integer casting ((int)$val) or intval() only if you know the field is numeric, then validate the result is within expected range.

References

  • https://nvd.nist.gov/vuln/detail/CVE-2025-13673

Frequently asked questions about CVE-2025-13673

What is CVE-2025-13673?

CVE-2025-13673 is a security vulnerability identified in tutor. This security advisory provides detailed technical analysis of the vulnerability, exploit methodology, affected versions, and complete remediation guidance.

Is there a PoC (proof of concept) for CVE-2025-13673?

Yes. This writeup includes proof-of-concept details and a technical exploit breakdown for CVE-2025-13673. Review the analysis sections above for the PoC walkthrough and code examples.

How does CVE-2025-13673 get exploited?

The technical analysis section explains the vulnerability mechanics, attack vectors, and exploitation methodology affecting tutor. PatchLeaks publishes this information for defensive and educational purposes.

What products and versions are affected by CVE-2025-13673?

CVE-2025-13673 affects tutor. Check the affected-versions section of this advisory for specific version ranges, vulnerable configurations, and compatibility information.

How do I fix or patch CVE-2025-13673?

The patch analysis section provides guidance on updating to patched versions, applying workarounds, and implementing compensating controls for tutor.

What is the CVSS score for CVE-2025-13673?

The severity rating and CVSS scoring for CVE-2025-13673 affecting tutor is documented in the vulnerability details section. Refer to the NVD entry for the current authoritative score.