The Exploit
An unauthenticated attacker can read any email logged by the Post SMTP plugin, including password reset links and account credentials, by directly requesting the email log view without authentication.
GET /wp-admin/admin.php?page=postman_email_log&email=1 HTTP/1.1
Host: target.wordpress.local
Cookie: (empty)
The server responds with a 200 OK and renders the full contents of email log entry #1 in plaintext HTML. An attacker observes the sender, recipient, subject, and complete email body — including password reset links sent by WordPress or other plugins. By incrementing the email parameter, the attacker enumerates all logged emails without authentication.
What the Patch Did
Before
// only do this for administrators
if ( PostmanUtils::isAdmin() ) {
$this->logger->trace( 'handling view item' );
$postid = absint( $_REQUEST ['email'] );
$post = get_post( $postid );
After
// Check if user has permission to view email logs
if ( ! current_user_can( Postman::MANAGE_POSTMAN_CAPABILITY_LOGS ) ) {
wp_die( __( 'Sorry, you are not allowed to view email logs.', 'post-smtp' ) );
}
$this->logger->trace( 'handling view item' );
$postid = absint( $_REQUEST ['email'] );
$post = get_post( $postid );
The patch replaced a custom PostmanUtils::isAdmin() check — which did not enforce WordPress capability-based access control — with the standard current_user_can() function paired with an explicit capability constant Postman::MANAGE_POSTMAN_CAPABILITY_LOGS. When the capability check fails, wp_die() terminates execution and returns an error page. The same fix was applied to four separate request handlers: the email log view, email transcript view, AJAX log retrieval, and AJAX log deletion endpoints.
Root Cause
CWE-862: Missing Authorization. The email parameter in $_REQUEST flows directly into get_post() without first verifying that the current user holds the MANAGE_POSTMAN_CAPABILITY_LOGS capability. The custom PostmanUtils::isAdmin() function checked only whether a user held the WordPress administrator role — a role-based check that does not implement proper authorization semantics in plugin contexts. An unauthenticated user fails the role check but the code path that reads the email post still executes if the condition is never evaluated or if the function returns true under unexpected conditions. The real vulnerability is that the authorization check happened after the request parameter was accepted, meaning a request with email=1 would pass parameter parsing before encountering any capability guard. Multiple AJAX handlers in PostmanEmailLogs.php had no authorization checks at all before processing $_GET['action'] and $_POST['action'] values.
Why It Works
The load-bearing line is if ( ! current_user_can( Postman::MANAGE_POSTMAN_CAPABILITY_LOGS ) ). If you removed it, the wp_die() would never execute, and the function would proceed to fetch and display the email post. The subsequent wp_die() call is also critical — without it, the function would return silently and the email data would leak anyway. The engineer added explicit capability constants instead of role checks because current_user_can() respects both roles and fine-grained plugin capabilities, allowing administrators to grant log-viewing permissions to non-administrator accounts if needed. By placing the check at the entry point of each handler — before $_REQUEST or $_GET are parsed for the email ID — the patch ensures that even a valid email ID cannot be used if the user lacks permission. The fix to AJAX handlers uses wp_send_json_error() instead of wp_die() to return a JSON response that client-side code can handle gracefully, following WordPress AJAX conventions.
Hardening Checklist
-
Audit all request handlers for missing
current_user_can()checks. Search the codebase for$_REQUEST,$_GET,$_POSTparsing in functions that read or modify data, and confirm each is guarded bycurrent_user_can()with an explicit capability string at the function entry point, not at arbitrary conditional branches. -
Replace role-based checks (
if ( is_admin() ), custom role detection) with capability-based authorization. Use WordPress'scurrent_user_can()API exclusively and define plugin-specific capabilities in the capability mapping during plugin initialization, allowing granular permission delegation. -
Add nonce verification to all state-changing AJAX handlers. For
POSTactions like deletion or modification, callcheck_ajax_referer()immediately after the capability check to prevent cross-site request forgery that could bypass authorization. -
Test authorization logic with users in non-administrator roles. Create test user accounts with subscriber, contributor, and editor roles; verify that each request handler rejects them with appropriate HTTP status codes (403 Forbidden or 200 with wp_die() / wp_send_json_error) rather than silently proceeding or redirecting to login.
-
Implement output escaping on sensitive data (email content, passwords, tokens). Even after authorization is fixed, apply
wp_kses_post()oresc_html()to email bodies before rendering, and consider not logging password reset links at all — store only a sanitized reference to the email event.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-11833