Skip to content

CVE-2026-10835: Verified Repro With Script Download

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 Variant found 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://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()
Variants tested

Alternate entry point to the same SQL-injection sink as the parent CVE-2026-10835 claim. The parent repro exploited salesmanago_export_count_contacts (ExportController::countContacts -> ExportModel::getExportContactsQuery(true) -> $wpdb->get_var). This variant uses the sibling AJAX action salesmanago_export_contacts (…

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.

Variant Analysis & Alternative Triggers for CVE-2026-10835

Versions: versions (as tested): confirmed on 3.11.2; blocked on

A distinct alternate-trigger variant of CVE-2026-10835 was found and confirmed end-to-end. The parent reproduction exploited the salesmanago_export_count_contacts AJAX action (ExportController::countContacts()ExportModel::getExportContactsQuery(true)$wpdb->get_var()). This variant reaches the same SQL-injection sink — ExportModel::getExportContactsQuery() interpolating the unsanitized $this->dateFrom directly into A.post_date >= '{$this->dateFrom}' — via the sibling AJAX action salesmanago_export_contacts (ExportController::exportContacts()getExportContactsQuery(false)$wpdb->get_results()). It relies on the same two defects as the parent: (a) SecureHelper::validate_ajax_nonce() returns false early for non-admins without dying, and the controller ignores the return value, so an authenticated Subscriber reaches the query with no nonce; and (b) parseArgs() assigns $data->dateFrom to $this->dateFrom with no sanitization. The variant was confirmed on vulnerable 3.11.2 (time-based blind SLEEP() delivered through the export_contacts action produced response delays scaling linearly with N: 0.073s → 6.068s → 12.012s) and was ruled out as a bypass on fixed 3.11.3 (the patched validate_ajax_nonce() dies with HTTP 403 for the Subscriber, the handler adds a manage_options gate, parseArgs() validates dateFrom, and getExportContactsQuery() uses $wpdb->prepare()). Outcome: alternate-trigger variant, not a bypass.

Fix Coverage / Assumptions

Invariant the 3.11.3 fix relies on: every code path that reaches the getExportContactsQuery() SQL sink (or any $wpdb call with user input) is gated by SecureHelper::validate_ajax_nonce() and a per-handler current_user_can('manage_options') check, and the only attacker-controlled field interpolated into SQL (dateFrom) is both validated (strict date format) and parameterized (%s).

Code paths the fix explicitly covers: all five export AJAX actions registered in ExportController::registerActions()salesmanago_export_count_contacts, salesmanago_export_contacts, salesmanago_export_count_events, salesmanago_export_events, salesmanago_export_products. Each now dies 403 for non-admins, validates dateFrom/dateTo, and the contacts query is $wpdb->prepare()-d. The events path (getEventsData) already routed through WooCommerce's parameterized wc_get_orders; the products path only interpolated int-cast LIMIT/OFFSET, now also prepare()-d.

What the fix does NOT cover (evaluated, no gap found): I inspected every other entry point that could reach a SQL sink with attacker-controlled input — the Admin.php AJAX actions (refresh_owners, refresh_catalogs, generate_swjs, wp_ajax_trash_post), the REST endpoints (salesmanago/v2/callbackApiV3 token-gated; salesmanago/v1/cart int-cast cart recovery), the CronController (token compared with hash_equals), and all remaining $wpdb call sites (AdminModel, ProductCatalogModel). None interpolate attacker-controlled strings into SQL. The dateTo field was never injectable (strtotime()+date() collapse it to a clean date). The fix is complete for the SQL-injection class. See bundle/vuln_variant/patch_analysis.md.

Variant / Alternate Trigger

Entry point: POST /wp-admin/admin-ajax.php with action=salesmanago_export_contacts, sent by an authenticated Subscriber (no nonce required in 3.11.2). The request body parameter data is a base64 JSON object containing the injected dateFrom and packageCount=1 (the latter is required to pass exportContacts()'s if ( $this->ExportModel->getPackageCount() ) guard so the vulnerable query actually executes).

Code path (vulnerable 3.11.2):

  • src/Admin/Controller/ExportController.php line 64 — registration wp_ajax_salesmanago_export_contacts → exportContacts.
  • src/Admin/Controller/ExportController.php lines 122–168 — exportContacts():
    • line 123: SecureHelper::validate_ajax_nonce( 'salesmanago_count_contacts' ) (note the pre-existing typo'd action name; for a Subscriber the early-return false happens regardless).
    • line 141: $this->ExportModel->parseArgs() (sets unsanitized dateFrom).
    • line 130: $query = $this->ExportModel->getExportContactsQuery( false ) (the sink; $count=false adds LIMIT/OFFSET instead of the COUNT(*) wrapper used by countContacts).
    • line 131: $results = $this->db->get_results( $query, ARRAY_A ) (the SQL executes here, with the injected SLEEP()/UNION).
  • src/Admin/Model/ExportModel.php lines 138–148 — parseArgs(): unsanitized $this->dateFrom = $data->dateFrom.
  • src/Admin/Model/ExportModel.php lines 341–406 — getExportContactsQuery(): A.post_date >= '{$this->dateFrom}' (string interpolation).
  • src/Includes/SecureHelper.php lines 81–104 — validate_ajax_nonce(): returns false for non-admins without dying.

Why this is materially distinct from the parent (not a relabeling): different AJAX action/endpoint, different controller method, different $wpdb primitive (get_results row-set vs get_var scalar count), an extra packageCount guard the attacker must satisfy, and a different exploitation primitive (the count scalar is not echoed by exportContacts — its rows flow to prepareContactsToExport + the SALESmanago API export — so direct UNION extraction via the HTTP response is blind here, whereas countContacts echoes the count; time-based blind SLEEP works on both).

  • Package/component affected: SALESmanago & Leadoo WordPress plugin (slug salesmanago), src/Admin/Controller/ExportController.php (exportContacts()) sharing src/Admin/Model/ExportModel.php (parseArgs() + getExportContactsQuery()) and src/Includes/SecureHelper.php (validate_ajax_nonce()).
  • Affected versions (as tested): confirmed on 3.11.2; blocked on 3.11.3. Same affected range as the parent CVE (≤ 3.11.2).
  • Risk level: High. An authenticated Subscriber (lowest privileged role) can append arbitrary SQL to the export-contacts query via the salesmanago_export_contacts action and exfiltrate arbitrary database contents (user hashes, secret keys, any table) using time-based/boolean blind techniques. CVSS parity with the parent (7.7).

Impact Parity

  • Disclosed/claimed maximum impact (parent/variant): authenticated SQL injection allowing a Subscriber to read sensitive data from the database.
  • Reproduced impact from this variant run: time-based blind SQL injection via the salesmanago_export_contacts action — SLEEP(N) executed inside the SQL engine with response delay scaling linearly with N (0.073s → 6.068s → 12.012s), from an authenticated Subscriber with no nonce. This is the same read primitive as the parent, demonstrated through the alternate endpoint.
  • Parity: full for the SQL-injection read primitive (same sink, same auth level, same trust boundary). The parent additionally demonstrated a direct UNION-based count extraction via the echoed count field; that specific direct-echo extraction is not available on exportContacts (rows are not echoed), but the same data is extractable via time-based/boolean blind SQLi (the demonstrated SLEEP primitive is the foundation for both).
  • Not demonstrated: a full dump of password hashes via this variant (out of scope for a variant reproduction; the demonstrated blind-SLEEP primitive is sufficient to mount such extraction with standard SQLi tooling).

Root Cause

The same underlying bug is reachable because ExportController registers two authenticated AJAX actions (salesmanago_export_count_contacts and salesmanago_export_contacts) that both call the same ExportModel::getExportContactsQuery() sink, and both rely on the same SecureHelper::validate_ajax_nonce() authorization helper. In 3.11.2 that helper returns false for non-admins without dying, and both controllers ignore the return value, so a Subscriber reaches the identical unsanitized dateFrom→SQL interpolation from either endpoint. The root cause is therefore identical (shared sink + shared input source + shared auth-bypass defect); see bundle/vuln_variant/root_cause_equivalence.json.

Fix reference: WordPress.org plugin release salesmanago.3.11.3.zip (https://downloads.wordpress.org/plugin/salesmanago.3.11.3.zip); the 3.11.3 diff is analyzed in bundle/vuln_variant/patch_analysis.md. No public git commit is available (the plugin is distributed as a WordPress.org zip); the exact tested source is pinned by release version + zip SHA-256 in bundle/vuln_variant/source_identity.json.

Reproduction Steps

  1. Reference script: bundle/vuln_variant/reproduction_steps.sh (self-contained; reuses the plugin zips under bundle/artifacts/ and bundle/repro/wp-cli.phar, and writes its own 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. All HTTP interaction is executed inside the WordPress container via docker compose exec (the sandbox host cannot reach the Docker bridge network).
    • Runs the WP 5-minute install, seeds a minimal salesmanago_configuration option (so ExportController's constructor completes and the wp_ajax_ handlers register), copies the plugin into the container, activates it, creates a Subscriber user + two dummy posts, and logs in as the Subscriber via the real wp-login.php cookie flow.
    • Sends time-based blind SQLi payloads to the variant endpoint action=salesmanago_export_contacts with data = base64(JSON{dateFrom:"2000-01-01' OR SLEEP(N) OR '1'='2", packageCount:1}), measuring HTTP status and response time. The payload keeps SLEEP(N) as a standalone OR operand (so MySQL cannot skip it) while the constant-FALSE third OR operand yields 0 matching rows, so exportContacts skips the external SALESmanago API call and the delay is purely the in-DBMS SLEEP.
    • Repeats the flow against the fixed 3.11.3 build (negative control): sends the SLEEP(2) payload as a Subscriber and expects HTTP 403.
    • Writes runtime_manifest.json, validation_verdict.json, per-run summary.csv, and run.log under bundle/logs/vuln_variant/.
  3. Expected evidence: on vulnerable 3.11.2 the SLEEP(0/1/2) response times scale linearly (~6s per unit) with HTTP 200; on fixed 3.11.3 the Subscriber is blocked with HTTP 403 and no timing differential.

Evidence

  • Run log: bundle/logs/vuln_variant/run.log
  • Vulnerable (3.11.2) summary: bundle/logs/vuln_variant/vulnerable/summary.csv
    sleep0|200|0.073038
    sleep1|200|6.067677
    sleep2|200|12.012328
    baseline|200|0.010659
    
  • Fixed (3.11.3) summary: bundle/logs/vuln_variant/fixed/summary.csv
    sleep2|403|0.074118
    
  • Vulnerable sleep2 response body (bundle/logs/vuln_variant/vulnerable/req_sleep2.txt):
    {"packageSize":400,"packageCount":1,"lastExportedPackage":-1,...,"status":"done",
     "dateFrom":"2000-01-01' OR SLEEP(2) OR '1'='2","dateTo":"2026-07-05","count":0,...}
    200 12.012328
    
    The status:"done" + count:0 confirms 0 rows were returned (the 0-rows payload worked) and the 12.01s delay is purely the in-DBMS SLEEP(2).
  • Vulnerable baseline response body (no injection, bundle/logs/vuln_variant/vulnerable/req_baseline.txt): status:"done", 0.010659s — confirms the endpoint works and the SLEEP delay is from injection, not the export API call.
  • Fixed sleep2 response body (bundle/logs/vuln_variant/fixed/req_sleep2.txt):
    {"success":false,"data":{"message":"forbidden"}}
    403 0.074118
    
  • Environment: Docker wordpress:6.7-php8.2-apache + mysql:8.0; plugin versions 3.11.2 (vulnerable) and 3.11.3 (fixed) from downloads.wordpress.org. Exact source identity (zip SHA-256) in bundle/vuln_variant/source_identity.json.

Recommendations / Next Steps

  • The 3.11.3 fix already covers this variant (authorization die + manage_options gate + input validation + $wpdb->prepare()). No additional code change is required for salesmanago_export_contacts specifically.
  • Defense in depth (recommended for the maintainer): add an integration test that sends a SQLi payload (single quote, SLEEP, UNION) to every wp_ajax_salesmanago_export_* action as a Subscriber and asserts HTTP 403 + no timing differential, so a future regression that re-introduces an unguarded handler or string interpolation is caught automatically.
  • Static guard: add a repo-wide scan forbidding {$...} interpolation inside any SQL string passed to $wpdb->get_var/get_row/get_results/query, and forbidding wp_ajax_* handlers that do not begin with a manage_options (or equivalent) capability check.
  • Centralize authorization: the pre-existing typo (validate_ajax_nonce('salesmanago_count_contacts') in exportContacts) shows per-handler action-name strings are error-prone; consider deriving the expected action from $_REQUEST['action'] inside validate_ajax_nonce (as 3.11.3 now does) rather than passing it per-call.

Additional Notes

  • Bypass vs alternate trigger: this is an alternate trigger, not a bypass. The variant reproduces on the vulnerable 3.11.2 path and is blocked (HTTP 403) on the fixed 3.11.3 path. The script exits 1 (alternate-trigger / no bypass); it would exit 0 only if the SLEEP also executed on the fixed version.
  • Idempotency confirmation: reproduction_steps.sh was run five consecutive times; runs 2–5 (after the payload was corrected to keep SLEEP as a standalone OR operand) all completed without crashing and produced identical confirming results (vulnerable SLEEP scaling yes, fixed 403). The Docker stack is fully torn down (docker compose down -v) at the end of each instance, so each run starts from a clean MySQL volume.
  • Payload note: an initial OR SLEEP(N) AND '1'='2 payload produced no timing differential because MySQL constant-folded the constant-FALSE AND and skipped evaluating SLEEP. The corrected OR SLEEP(N) OR '1'='2 payload keeps SLEEP as a standalone OR operand (which MySQL cannot skip) while still returning 0 rows — this is the form used in the final script and artifacts.
  • Negative search boundedness: fewer than 3 materially distinct SQLi candidates exist in this plugin. The only sink that interpolates attacker-controlled input is getExportContactsQuery() (reached by countContacts and exportContacts). The events path uses WooCommerce's parameterized wc_get_orders; the products path uses int-cast LIMIT/OFFSET; all other $wpdb sites are either already prepare()-d or use constant queries; the REST/cart/cron surfaces do not reach a raw-SQL sink. These are documented in patch_analysis.md rather than re-run as "attempts," since they are rule-outs by code inspection, not runtime variants.

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.