Feb 28, 2026·5 min read·6 visits
PMD versions prior to 7.22.0 fail to properly escape source code metadata in HTML reports, leading to Stored XSS. Attackers can embed payloads in code being analyzed to execute JavaScript when the report is viewed.
A Stored Cross-Site Scripting (XSS) vulnerability exists in the HTML report generation components of PMD, a popular extensible multilanguage static code analyzer. The flaw allows attackers to inject malicious JavaScript into source code comments or string literals, which are subsequently rendered unescaped in PMD's HTML reports. This affects the 'vbhtml' and 'yahtml' renderers, as well as the suppression reporting in the standard 'html' renderer. Successful exploitation executes arbitrary code in the context of the user viewing the report, potentially compromising CI/CD dashboard sessions.
CVE-2026-28338 constitutes a Stored Cross-Site Scripting (XSS) flaw within PMD, specifically affecting its HTML reporting modules. PMD is designed to scan source code for programming flaws, often integrating into Continuous Integration (CI) pipelines to generate static analysis reports. The vulnerability manifests when PMD processes untrusted source code containing crafted payloads in string literals or suppression comments.
The affected components are the VBHTMLRenderer, YAHTMLRenderer, and the suppression reporting logic within the standard HTMLRenderer. These components are responsible for transforming analysis results—including rule descriptions, file names, and specific code snippets—into human-readable HTML tables. By failing to sanitize this input during the transformation process, PMD allows the injected content to break out of the intended HTML structure.
While the primary impact is on the developer or auditor viewing the report, the context of this exploitation is often critical infrastructure. If these reports are hosted on a build server (e.g., Jenkins, GitLab CI), an attacker could hijack the session of an authenticated user, potentially gaining access to the build environment or other sensitive projects visible to that user.
The root cause of this vulnerability is the direct concatenation of untrusted data into HTML output streams without context-aware encoding. PMD's architecture involves 'Rules' that identify violations and generating 'Report' objects. These objects contain metadata extracted directly from the source code, such as the content of a duplicate string literal (detected by AvoidDuplicateLiterals) or the text of a suppression comment (e.g., // NOPMD - false positive).
In the vulnerable versions, the renderers utilized StringBuilder or string concatenation to construct the HTML output. For instance, the VBHTMLRenderer would append rv.getDescription() directly into a table cell. While PMD does employ StringUtil.escapeJava() for some inputs, this utility effectively escapes Java-specific control characters (like newlines or quotes) but does not encode HTML entities (like < or >).
Consequently, a string literal such as "><script>... remains syntactically valid Java but becomes a malicious HTML tag when rendered. The application assumed that the internal representation of violation data was safe or purely informational, neglecting the fact that this data originates from potentially hostile source code.
The remediation for CVE-2026-28338 was applied in commit c140c0e1de5853a08efb84c9f91dfeb015882442. The fix involves the systematic application of StringEscapeUtils.escapeHtml4() to all data points reflected in the DOM.
Vulnerable Code (VBHTMLRenderer.java):
In the pre-patch version, the renderer directly appended the filename and description to the buffer. The filename and desc variables flow directly from the source code analysis.
// Vulnerable implementation
private void renderViolations(Iterator<RuleViolation> violations) throws IOException {
// ...
sb.append("<tr>");
sb.append("<td class=\"filename\">" + filename + "</td>");
sb.append("<td class=\"rule\">" + rv.getRule().getName() + "</td>");
sb.append("<td class=\"desc\">" + desc + "</td>");
// ...
}Patched Code (VBHTMLRenderer.java):
The patch introduces a helper method escape() which wraps the Apache Commons StringEscapeUtils. This ensures that characters like < are converted to < before rendering.
// Patched implementation
private String escape(String s) {
return StringEscapeUtils.escapeHtml4(s);
}
private void renderViolations(Iterator<RuleViolation> violations) throws IOException {
// ...
sb.append("<tr>");
sb.append("<td class=\"filename\">" + escape(filename) + "</td>");
sb.append("<td class=\"rule\">" + escape(rv.getRule().getName()) + "</td>");
sb.append("<td class=\"desc\">" + escape(desc) + "</td>");
// ...
}Critique of the Fix:
While the HTML entity encoding effectively neutralizes the XSS vector, the patch implementation for externalInfoUrl uses URLEncoder.encode(infoUrl, "UTF-8"). This is technically incorrect for full URLs, as it percent-encodes protocol separators (e.g., https:// becomes https%3A%2F%2F). This likely breaks the hyperlinks in the generated report, rendering the documentation links dysfunctional, although it does prevent attribute injection.
Exploiting this vulnerability requires the attacker to introduce a malicious payload into a file that will be scanned by PMD. This is a common scenario in open-source projects accepting Pull Requests or in enterprise environments where developers commit code to shared repositories.
Step 1: Payload Construction
The attacker identifies a PMD rule that reflects code content. The AvoidDuplicateLiterals rule is an ideal candidate because it reports the exact string literal content in the violation message. The attacker creates a Java class with a crafted string:
public class Poc {
// The payload closes the previous td, opens an img tag with an event handler
String xss = "><img src=x onerror=alert(document.domain)>";
String xss2 = "><img src=x onerror=alert(document.domain)>"; // Duplicate to trigger rule
}Step 2: Execution
The victim (e.g., a CI server or a lead developer) runs PMD against the codebase using a vulnerable renderer:
pmd check -d . -R rulesets/java/quickstart.xml -f vbhtml -r report.html
Step 3: Trigger
When the victim opens report.html, the browser parses the unescaped payload. The resulting HTML structure will look like:
<td>The String literal "<img src=x onerror=alert(document.domain)>" appears 2 times</td>
The script executes immediately, granting the attacker access to the context in which the report is viewed.
The impact of CVE-2026-28338 is classified as Medium (CVSS 6.8), largely due to the requirement for user interaction (UI:R) and the complexity (AC:H) involved in the attack chain. However, in specific environments, the consequences can be severe.
Confidentiality (High): If the report is viewed on a centralized CI/CD dashboard (e.g., Jenkins artifact viewer), the XSS payload executes within the origin of that dashboard. This allows the attacker to steal session cookies, potentially compromising the build system or other repositories accessible to the victim.
Integrity (High): The attacker can modify the content of the report to hide genuine security issues or mislead the auditor. Furthermore, if the session is hijacked, the attacker could manipulate build configurations or inject backdoors into the software supply chain via the compromised user's account.
Availability (None): The vulnerability does not directly cause denial of service to the PMD application itself or the host system, although the report could be rendered unusable.
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
PMD PMD | < 7.22.0 | 7.22.0 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-28338 |
| CWE ID | CWE-79 |
| CVSS v3.1 | 6.8 (Medium) |
| Attack Vector | Network |
| Attack Complexity | High |
| Privileges Required | None |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')