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.
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:
export default function (input) {
return new SafeString(ansiConverter.toHtml(input));
};
The AnsiToHtml constructor at line 4 does not set escapeForHtml: true:
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:
<pre class="status-details__message"><code>{{ansi statusMessage}}</code></pre>
...
<pre class="{{b 'status-details' 'trace'}}"><code>{{ansi statusTrace}}</code></pre>
Source — plugins/junit-xml-plugin/src/main/java/io/qameta/allure/junitxml/JunitXmlPlugin.java:307-308:
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:
const safeText = escapeExpression(text);
return new SafeString(`<a href="${safeText}" ...>${safeText}</a>`);
<?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>
allure generate /path/to/results-with-malicious-xml -o /tmp/allure-report
allure open /tmp/allure-report
<img onerror> payloads execute JavaScript in the viewer's browser.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.
{
"nvd_published_at": null,
"severity": "MODERATE",
"github_reviewed": true,
"github_reviewed_at": "2026-06-19T21:15:53Z",
"cwe_ids": [
"CWE-79"
]
}