CVE-2023-32984
Description
Jenkins TestNG Results Plugin 730.v4c5283037693 and earlier has a stored XSS vulnerability due to unescaped values from report files.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins TestNG Results Plugin 730.v4c5283037693 and earlier has a stored XSS vulnerability due to unescaped values from report files.
Vulnerability
Description The Jenkins TestNG Results Plugin versions 730.v4c5283037693 and earlier do not escape several values parsed from TestNG report files when displaying them on the plugin's test information pages. This results in a stored cross-site scripting (XSS) vulnerability [1][3]. The plugin fails to sanitize user-controlled data before rendering it in the browser, allowing malicious scripts to be embedded.
Exploitation
An attacker who can provide a crafted TestNG report file (e.g., by submitting test results to a Jenkins job) can inject arbitrary HTML and JavaScript into the report display. No special privileges are required beyond the ability to upload or cause the generation of a malicious report file. The stored XSS will execute when other users, including administrators, view the affected test information pages.
Impact
A successful exploit allows the attacker to execute arbitrary JavaScript in the context of the victim's browser session. This can lead to session hijacking, credential theft, or unauthorized actions on the Jenkins instance, such as modifying configurations or accessing sensitive data.
Mitigation
The vulnerability has been addressed in a later version of the plugin. The fix involves escaping output values using hudson.Util.escape() as shown in the commit [2]. Users should upgrade to the latest available version of the TestNG Results Plugin. No workaround other than upgrading is mentioned.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.plugins:testng-pluginMaven | < 730.732.v959a | 730.732.v959a |
Affected products
2- Range: 0
Patches
15f3d83ca56c0SECURITY-3047
7 files changed · +47 −18
src/main/java/hudson/plugins/testng/parser/ResultsParser.java+2 −1 modified@@ -18,6 +18,7 @@ import java.util.logging.Logger; import hudson.FilePath; +import hudson.Util; import hudson.plugins.testng.results.ClassResult; import hudson.plugins.testng.results.MethodResult; import hudson.plugins.testng.results.MethodResultException; @@ -301,7 +302,7 @@ private void endLine() { if (currentMethod != null) { - reporterOutputBuilder.append(currentLine).append("<br/>"); + reporterOutputBuilder.append(Util.escape(currentLine)).append("<br/>"); } }
src/main/java/hudson/plugins/testng/results/PackageResult.java+5 −4 modified@@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; +import hudson.Util; import hudson.model.Run; import hudson.plugins.testng.util.FormatUtil; import org.kohsuke.stapler.StaplerRequest; @@ -146,17 +147,17 @@ private String getMethodExecutionTableContent(List<MethodResult> mrList) { for (MethodResult mr : mrList) { sb.append("<tr><td align=\"left\">"); - sb.append("<a href=\"").append(mr.getUpUrl()).append("\">"); - sb.append(mr.getParent().getName()).append(".").append(mr.getName()); + sb.append("<a href=\"").append(Util.escape(mr.getUpUrl())).append("\">"); + sb.append(Util.escape(mr.getParent().getName())).append(".").append(Util.escape(mr.getName())); sb.append("</a>"); sb.append("</td><td align=\"left\">"); - sb.append(mr.getDescription()); + sb.append(Util.escape(mr.getDescription())); sb.append("</td><td align=\"center\">"); sb.append(FormatUtil.formatTime(mr.getDuration())); sb.append("</td><td align=\"center\">"); sb.append(mr.getStartedAt()); sb.append("</td><td align=\"center\"><span class=\"").append(mr.getCssClass()).append("\">"); - sb.append(mr.getStatus()); + sb.append(Util.escape(mr.getStatus())); sb.append("</span></td></tr>"); } return sb.toString();
src/main/java/hudson/plugins/testng/util/FormatUtil.java+19 −0 modified@@ -79,6 +79,25 @@ public static String escapeString(String str) { return str; } + public static String escapeJS(String str) { + if (str == null) { + return ""; + } + StringBuilder buf = new StringBuilder(str.length() + 64); + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (ch == '<') buf.append("<"); + else if (ch == '>') buf.append(">"); + else if (ch == '&') buf.append("&"); + else if (ch == '\'') buf.append("'"); + else if (ch == '\"') buf.append("""); + else if (ch == ':') buf.append(":"); + else if (ch == '%') buf.append("%"); + else buf.append(ch); + } + return buf.toString(); + } + /** * Formats the stack trace for easier readability * @param stackTrace a stack trace
src/main/java/hudson/plugins/testng/util/TestResultHistoryUtil.java+7 −6 modified@@ -2,6 +2,7 @@ import java.util.List; +import hudson.Util; import hudson.model.Run; import hudson.plugins.testng.TestNGTestResultBuildAction; import hudson.plugins.testng.results.ClassResult; @@ -111,19 +112,19 @@ private static String printTestsUrls(List<MethodResult> methodResults) { htmlStr.append("</OL></LI>"); } firstGroup = false; - testName = methodResult.getParentTestName(); - suiteName = methodResult.getParentSuiteName(); + testName = Util.escape(methodResult.getParentTestName()); + suiteName = Util.escape(methodResult.getParentSuiteName()); htmlStr.append("<LI style=\"list-style-type:none\"><b>").append(suiteName).append(" / ").append(testName).append("</b>"); htmlStr.append("<OL start=\"").append(testIndex).append("\">"); } htmlStr.append("<LI>"); if (methodResult.getParent() instanceof ClassResult) { - htmlStr.append("<a href=\"").append(methodResult.getUpUrl()); + htmlStr.append("<a href=\"").append(Util.escape(methodResult.getUpUrl())); htmlStr.append("\">"); - htmlStr.append(((ClassResult)methodResult.getParent()).getCanonicalName()); - htmlStr.append(".").append(methodResult.getName()).append("</a>"); + htmlStr.append(Util.escape(((ClassResult) methodResult.getParent()).getCanonicalName())); + htmlStr.append(".").append(Util.escape(methodResult.getName())).append("</a>"); } else { - htmlStr.append(methodResult.getName()); + htmlStr.append(Util.escape(methodResult.getName())); } htmlStr.append("</LI>"); testIndex++;
src/main/resources/hudson/plugins/testng/results/ClassResult/reportDetail.groovy+3 −1 modified@@ -1,5 +1,6 @@ package hudson.plugins.testng.results.ClassResult +import hudson.Functions import hudson.plugins.testng.util.FormatUtil import org.apache.commons.lang.StringUtils @@ -43,6 +44,7 @@ for (group in my.testRunMap.values()) { } tbody() { for(method in group.testMethods) { + def methodJsSafeName = Functions.jsStringEscape(method.safeName) tr() { td(align:"left") { a(href:"${method.upUrl}") { @@ -51,7 +53,7 @@ for (group in my.testRunMap.values()) { if (method.groups || method.testInstanceName || method.parameters?.size() > 0) { div(id:"${method.safeName}_1", style:"display:inline") { text(" (") - a(href:"javascript:showMore(\"${method.safeName}\")") { + a(href:"javascript:showMore(\"${methodJsSafeName}\")") { raw("…") } text(")")
src/main/resources/hudson/plugins/testng/results/MethodResult/summary.jelly+1 −1 modified@@ -13,7 +13,7 @@ <j:otherwise> <j:if test="${it.errorStackTrace != null}"> <h3>Stack Trace</h3> - <pre><j:out value="${it.errorStackTrace}"/></pre> + <pre><st:out value="${it.errorStackTrace}"/></pre> </j:if> </j:otherwise> </j:choose>
src/main/resources/hudson/plugins/testng/TestNGTestResultBuildAction/reportDetail.groovy+10 −5 modified@@ -1,5 +1,6 @@ package hudson.plugins.testng.TestNGTestResultBuildAction +import hudson.Functions import hudson.plugins.testng.util.FormatUtil f = namespace(lib.FormTagLib) @@ -29,12 +30,14 @@ if (my.result.failCount != 0) { } tbody() { for (failedTest in my.result.failedTests) { + def failedTestSafeId = Functions.jsStringEscape(failedTest.id) + def failedTestSafeUpUrl = Functions.jsStringEscape(failedTest.upUrl) tr() { td(align: "left") { - a(id: "${failedTest.id}-showlink", href:"javascript:showStackTrace('${failedTest.id}', '${failedTest.upUrl}/summary')") { + a(id: "${failedTest.id}-showlink", href:"javascript:showStackTrace('${failedTestSafeId}', '${failedTestSafeUpUrl}/summary')") { text(">>>") } - a(style: "display:none", id: "${failedTest.id}-hidelink", href:"javascript:hideStackTrace('${failedTest.id}')") { + a(style: "display:none", id: "${failedTest.id}-hidelink", href:"javascript:hideStackTrace('${failedTestSafeId}')") { text("<<<") } text(" ") @@ -114,7 +117,7 @@ table(id:"all-tbl", border:"1px", class:"pane sortable") { def prevPkg = pkg.previousResult tr() { td(align: "left") { - a(href:"${pkg.name}") { text("${pkg.name}") } + a(href:"${FormatUtil.escapeJS(pkg.name)}") { text("${pkg.name}") } } td(align: "center") { text("${FormatUtil.formatTime(pkg.duration)}") @@ -168,13 +171,15 @@ def printMethods(type, tableName, methodList, showMoreArrows) { } tbody() { for (method in methodList) { + def methodSafeId = Functions.jsStringEscape(method.id) + def methodSafeUpUrl = Functions.jsStringEscape(method.upUrl) tr() { td(align: "left") { if (showMoreArrows) { - a(id: "${method.id}-showlink", href:"javascript:showStackTrace('${method.id}', '${method.upUrl}/summary')") { + a(id: "${method.id}-showlink", href:"javascript:showStackTrace('${methodSafeId}', '${methodSafeUpUrl}/summary')") { text(">>>") } - a(style: "display:none", id: "${method.id}-hidelink", href:"javascript:hideStackTrace('${method.id}')") { + a(style: "display:none", id: "${method.id}-hidelink", href:"javascript:hideStackTrace('${methodSafeId}')") { text("<<<") } text(" ")
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
1- Jenkins Security Advisory 2023-05-16Jenkins Security Advisories · May 16, 2023