Allure Report: Stored XSS via unescaped ANSI helper in status message/trace rendering
Description
Summary
The ansi.js Handlebars helper in allure-generator passes user-controlled statusMessage and statusTrace values from test result files through the ansi-to-html library and wraps the output in Handlebars SafeString without HTML escaping. Since ansi-to-html does not escape HTML entities by default, an attacker who can influence test result content (e.g., via crafted JUnit XML failure messages) can inject arbitrary JavaScript that executes when anyone views the generated Allure report.
Details
The vulnerability is an incomplete fix — commit 4c64b19 (PR #3271) fixed XSS in linky.js and text-with-links.js by adding escapeExpression(), but the same pattern in ansi.js was not addressed.
Vulnerable sink — allure-generator/src/main/javascript/helpers/ansi.js:10-11: ``javascript export default function (input) { return new SafeString(ansiConverter.toHtml(input)); }; ``
The AnsiToHtml constructor at line 4 does not set escapeForHtml: true: ``javascript const ansiConverter = new AnsiToHtml({ fg: "black", bg: "black", newline: true, }); ``
The ansi-to-html library (v0.7.2) defaults escapeForHtml to false, meaning HTML entities in the input pass through unchanged. Wrapping the result in SafeString tells Handlebars to skip its auto-escaping, so the raw HTML reaches the browser.
Template usage — allure-generator/src/main/javascript/blocks/status-details/status-details.hbs:7,10: ``handlebars {{ansi statusMessage}} ... {{ansi statusTrace}} ``
Source — plugins/junit-xml-plugin/src/main/java/io/qameta/allure/junitxml/JunitXmlPlugin.java:307-308: ``java result.setStatusMessage(element.getAttribute(MESSAGE_ATTRIBUTE_NAME)); result.setStatusTrace(element.getValue()); ``
These values are read directly from XML attributes with no sanitization. The same pattern exists in TRX, xUnit XML, xctest, and Allure1/2 plugins.
Contrast with the fixed helper — linky.js (post-fix) correctly escapes before wrapping in SafeString: ``javascript const safeText = escapeExpression(text); return new SafeString(${safeText}); ``
PoC
- Create a malicious JUnit XML test result file:
<?xml version="1.0" encoding="UTF-8"?>
Stack trace: <img src=x onerror=alert('statusTrace_XSS')>
- Generate an Allure report:
allure generate /path/to/results-with-malicious-xml -o /tmp/allure-report
- Open the report and navigate to the failed test case:
allure open /tmp/allure-report
- When viewing the test's status details, the `` payloads execute JavaScript in the viewer's browser.
Impact
- Arbitrary JavaScript execution in the browser of anyone viewing the generated Allure report
- Cookie theft, session hijacking if the report is served from a domain with active sessions (e.g., CI dashboards)
- Data exfiltration — the injected script can read the full report content and send it to an attacker-controlled server
- Attack vectors: A malicious dependency that throws crafted exception messages, a CI pipeline processing test results from untrusted pull requests, or a contributor submitting test files containing XSS payloads
- Allure reports are commonly hosted on CI/CD platforms (Jenkins, GitLab, GitHub Actions artifacts) where session cookies may be present
Recommended
Fix
Configure AnsiToHtml with escapeForHtml: true to escape HTML entities while preserving ANSI-to-HTML conversion:
import AnsiToHtml from "ansi-to-html";
import {SafeString} from "handlebars/runtime";
const ansiConverter = new AnsiToHtml({
fg: "black",
bg: "black",
newline: true,
escapeForHtml: true, // Escape HTML entities in non-ANSI input
});
export default function (input) {
return new SafeString(ansiConverter.toHtml(input));
};
This is the correct approach because it preserves the ANSI escape sequence → HTML conversion (colored output) while ensuring that any non-ANSI HTML in the input is safely escaped. The alternative of using escapeExpression() on the input would destroy ANSI sequences before they could be converted.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.qameta.allure:allure-generatorMaven | < 2.39.0 | 2.39.0 |
Affected products
1Patches
Vulnerability mechanics
Root cause
"The `ansi.js` Handlebars helper passes user-controlled input through `ansi-to-html` without setting `escapeForHtml: true` and wraps the result in `SafeString`, allowing unescaped HTML to reach the browser."
Attack vector
An attacker who can influence test result content (e.g., via crafted JUnit XML failure messages) injects HTML/JavaScript payloads into `statusMessage` or `statusTrace` fields. The `ansi.js` helper passes these values through `ansi-to-html` without HTML escaping and wraps the output in `SafeString`, bypassing Handlebars auto-escaping [ref_id=1][ref_id=2]. When a victim views the generated Allure report, the payload executes as arbitrary JavaScript in their browser.
Affected code
The vulnerable sink is `allure-generator/src/main/javascript/helpers/ansi.js:10-11`, which passes user-controlled `statusMessage` and `statusTrace` values through `ansi-to-html` (with `escapeForHtml` defaulting to `false`) and wraps the result in `SafeString` without HTML escaping. The data originates in `plugins/junit-xml-plugin/src/main/java/io/qameta/allure/junitxml/JunitXmlPlugin.java:307-308`, where XML attributes are read without sanitization. The same pattern exists in TRX, xUnit XML, xctest, and Allure1/2 plugins.
What the fix does
The patch [patch_id=6633379] fixed XSS in `linky.js` and `text-with-links.js` by adding `escapeExpression()` before wrapping in `SafeString`, but the same pattern in `ansi.js` was not addressed [ref_id=1][ref_id=2]. The recommended fix is to configure `AnsiToHtml` with `escapeForHtml: true`, which escapes HTML entities while preserving ANSI-to-HTML conversion, avoiding the need to call `escapeExpression()` on the input (which would destroy ANSI sequences).
Preconditions
- inputAttacker must be able to supply or influence test result files (e.g., JUnit XML, TRX, xUnit XML, xctest, Allure1/2) that are processed by allure-generator
- networkThe generated Allure report must be viewed by a victim in a browser
Reproduction
1. Create a malicious JUnit XML test result file with an XSS payload in the `message` attribute or element body: ```xml <?xml version="1.0" encoding="UTF-8"?> <testsuite name="XSSTest" tests="1" failures="1"> <testcase name="xssPayload" classname="com.example.Test"> <failure message="<img src=x onerror=alert(document.cookie)>"> Stack trace: <img src=x onerror=alert('statusTrace_XSS')> </failure> </testcase> </testsuite> ``` 2. Generate the report: `allure generate /path/to/results-with-malicious-xml -o /tmp/allure-report` 3. Open the report: `allure open /tmp/allure-report` 4. Navigate to the failed test case — the `<img onerror>` payloads execute JavaScript in the viewer's browser.
Generated on Jun 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.