The Exploit
No authentication is required. The Pro version of Dokan must be active, and store reviews must be enabled.
curl -s 'https://example.com/wp-json/dokan/v1/stores/1/reviews' | jq '.reviews[] | {id: .id, name: .reviewer.name, email: .reviewer.email, username: .reviewer.username, user_id: .reviewer.user_id}'
The response will include reviewer.email (the reviewer's email address), reviewer.username (their WordPress username), and reviewer.user_id (their numeric user ID) for every review on the store. An attacker can iterate through numeric store IDs (/stores/{id}/reviews) to harvest email addresses and usernames across all vendors.
What the Patch Did
Before (in includes/REST/StoreController.php, lines 831-856):
'name' => $user->user_login,
'email' => $user->user_email,
and
'name' => $item->comment_author,
'email' => $item->comment_author_email,
After (same file):
'name' => $user->display_name,
and
'name' => $item->comment_author,
The patch removes the email field from the API response entirely and replaces the user_login field with display_name. No capability check or additional permission gate was added — the vulnerability was purely an over-disclosure of data that exists in the response object. The developer chose to prune the response content rather than gate access to the endpoint.
Root Cause
CWE-200: Exposure of Sensitive Information to an Unauthorized Actor. The dataflow begins at the REST API endpoint /dokan/v1/stores/{id}/reviews, which is registered by the Dokan plugin with no authentication requirement. When an unauthenticated request arrives, the prepare_reviews_for_response method (called from the get_items method of StoreController) builds a response array for each review. For reviews authored by registered WordPress users, it populates reviewer.email with $user->user_email and reviewer.username with $user->user_login. For anonymous reviews (those left by unregistered users), it populates reviewer.email with the raw $item->comment_author_email from the database. The trust boundary is crossed when this internal user data — available only to the WordPress user database — is serialized into a REST API response accessible to any HTTP client without a valid nonce or authentication cookie.
Why It Works
The single load-bearing line removed from the patch is:
'email' => $user->user_email,
If an attacker were to simply undo that one deletion in the patched code, the full vulnerability would be restored — the API would again expose reviewer email addresses and usernames. The other changes ($user->display_name instead of $user->user_login, and the removal of $item->comment_author_email for anonymous reviews) are complementary but not individually decisive: keeping user_login would still expose a username, but the most damaging data — the email address — was the primary target. The engineer likely removed user_login and comment_author_email as defence-in-depth to prevent any secondary information leakage (e.g., email confirmation phishes or username enumeration for brute-force attacks), but the email field alone is the critical vector.
Hardening Checklist
- Audit all REST API response schemas for
user_email,comment_author_email, or anyuser_loginfields that are not explicitly needed by the frontend. Userest_prepare_{post_type}orrest_prepare_commentfilters to strip sensitive fields before the response is sent. - Implement a capability check on every endpoint that returns user data, even if the frontend appears to require Pro features. Use
current_user_can('read')or a custom capability likedokan_view_store_reviewsto gate access. - Add a
__return_falseonrest_authentication_errorsfor unauthenticated requests if you must keep the endpoint public, then explicitly check foris_user_logged_in()inside the endpoint handler before returning sensitive fields. Do not rely on the permission callback alone — check each field at serialization time. - Replace
user_loginwithdisplay_namein all REST API responses unless the username is required for a specific feature (e.g., mentioning a user in a notification). The display name cannot be used for brute-force login attempts. - Add
rest_prepare_dokan_store_reviewfilter documentation so future maintainers understand which fields are intentionally public and which are internal. Useregister_rest_field()orrest_register_field()with an explicitget_callbackthat performs a permission check.
References
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-3504
- Wordfence Threat Intelligence: https://www.wordfence.com/threat-intel/vulnerabilities/id/cve-2026-3504
- Vendor changelog (not publicly confirmed; see patch evidence in
templates/whats-new.php)