High severityNVD Advisory· Published Sep 21, 2022· Updated May 28, 2025
CVE-2022-41225
CVE-2022-41225
Description
Jenkins Anchore Container Image Scanner Plugin 1.0.24 and earlier does not escape content provided by the Anchore engine API, resulting in a stored cross-site scripting (XSS) vulnerability exploitable by attackers able to control API responses by Anchore engine.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.plugins:anchore-container-scannerMaven | < 1.0.25 | 1.0.25 |
Affected products
1- Range: unspecified
Patches
11b1a62ab8ab8Updates to ensure that rendered data is escaped, including
6 files changed · +123 −102
.gitignore+3 −0 modified@@ -18,5 +18,8 @@ target/ .idea *.iml +# hpi:run data +/work/ + # OS .DS_Store
pom.xml+4 −4 modified@@ -5,7 +5,7 @@ <parent> <groupId>org.jenkins-ci.plugins</groupId> <artifactId>plugin</artifactId> - <version>2.11</version> + <version>4.19</version> <relativePath /> </parent> @@ -26,12 +26,11 @@ <properties> <!-- Baseline Jenkins version you use to build the plugin. Users must have this version or newer to run. --> - <jenkins.version>1.625.3</jenkins.version> + <jenkins.version>2.222.4</jenkins.version> <!-- Java Level to use. Java 7 required when using core >= 1.612 --> <java.level>8</java.level> <!-- Jenkins Test Harness version you use to test the plugin. --> <!-- For Jenkins version >= 1.580.1 use JTH 2.x or higher. --> - <jenkins-test-harness.version>2.13</jenkins-test-harness.version> <!-- Other properties you may want to use: ~ hpi-plugin.version: The HPI Maven Plugin version used by the plugin.. ~ stapler-plugin.version: The Stapler Maven plugin version required by the plugin. @@ -89,7 +88,7 @@ <dependency> <groupId>org.jenkins-ci.plugins</groupId> <artifactId>structs</artifactId> - <version>1.7</version> + <version>1.24</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> @@ -198,6 +197,7 @@ <groupId>org.jenkins-ci.tools</groupId> <artifactId>maven-hpi-plugin</artifactId> <configuration> + <minimumJavaVersion>8</minimumJavaVersion> <webResources> <webResource> <directory>${project.build.directory}/external-resources</directory>
src/main/java/com/anchore/jenkins/plugins/anchore/AnchoreAction.java+15 −4 modified@@ -95,7 +95,7 @@ public String getGateStatus() { } public String getGateOutputUrl() { - return this.gateOutputUrl; + return encodeURL(this.gateOutputUrl); } public Map<String, String> getQueryOutputUrls() { @@ -119,10 +119,12 @@ public Map<String, String> getQueryOutputUrls() { try { // Fetch values in the map to verify the underlying map is functional if (null != this.queryOutputUrls) { - fixedQueryOutputUrls.putAll(this.queryOutputUrls); + for (Map.Entry<String, String> entry : this.queryOutputUrls.entrySet()) { + fixedQueryOutputUrls.put(entry.getKey(), encodeURL(entry.getValue())); + } } } catch (Exception e) { - String base_path = this.gateOutputUrl.substring(0, this.gateOutputUrl.lastIndexOf('/')); + String base_path = encodeURL(this.gateOutputUrl.substring(0, this.gateOutputUrl.lastIndexOf('/'))); int query_num = 0; for (String key : this.queryOutputUrls.keySet()) { fixedQueryOutputUrls.put(key, base_path + "/anchore_query_" + String.valueOf(++query_num) + ".json"); @@ -144,7 +146,7 @@ public JSONObject getGateSummary() { } public String getCveListingUrl() { - return cveListingUrl; + return encodeURL(cveListingUrl); } public String getGateReportUrl() { @@ -196,4 +198,13 @@ public AnchoreAction getPreviousResult() { } } } + + private static String encodeURL(String s) { + if (s == null) { + return s; + } + return s.replaceAll("\"", "%22") + .replaceAll("\n", "%0A") + .replaceAll("\r", ""); + } }
src/main/java/com/anchore/jenkins/plugins/anchore/BuildWorker.java+1 −2 modified@@ -546,8 +546,7 @@ private void runVulnerabilityListing() throws AbortException { JSONArray vulnArray = new JSONArray(); vulnArray.addAll(Arrays .asList(input, vulnJson.getString("vuln"), vulnJson.getString("severity"), vulnJson.getString("package"), - vulnJson.getString("fix"), - "<a href='" + vulnJson.getString("url") + "'>" + vulnJson.getString("url") + "</a>")); + vulnJson.getString("fix"), vulnJson.getString("url"))); dataJson.add(vulnArray); } }
src/main/resources/com/anchore/jenkins/plugins/anchore/AnchoreAction/index.jelly+8 −1 modified@@ -1,13 +1,20 @@ <?jelly escape-by-default='true'?> <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> - <l:layout norefresh="true"> + <l:layout title="${it.pageTitle}" norefresh="true"> <st:include it="${it.build}" page="sidepanel.jelly" /> <l:main-panel> <j:choose> <j:when test="${!empty(it.gateOutputUrl)}"> <link rel="stylesheet" type="text/css" href="${resURL}/plugin/anchore-container-scanner/css/bootstrap.min.css"/> + <style type="text/css"> + .page-header { + margin: 0; + padding-bottom: 0; + border-bottom: 0; + } + </style> <link rel="stylesheet" type="text/css" href="${resURL}/plugin/anchore-container-scanner/css/dataTables.bootstrap.min.css"/> <script type="text/javascript" src="${resURL}/plugin/anchore-container-scanner/js/jquery.min.js"></script>
src/main/webapp/js/renderOutput.js+92 −91 modified@@ -14,66 +14,47 @@ const severityLookup = { } function gateAction(source, type, val) { - var el = '<span>' + source + '</span>'; - if ((typeof source === 'string') && source.trim().toLowerCase().match(/(stop|go|warn)/g)) { - switch (source.trim().toLowerCase()) { - case 'stop': { - el = '<span style="display:none;">' + actionLookup[source.toLowerCase()] - + '</span><span class="label label-danger">' + source.toUpperCase() + '</span>'; - break; - } - case 'go': { - el = '<span style="display:none;">' + actionLookup[source.toLowerCase()] - + '</span><span class="label label-success">' + source.toUpperCase() + '</span>'; - break; - } - case 'warn': { - el = '<span style = "display:none;">' + actionLookup[source.toLowerCase()] - + '</span><span class="label label-warning">' + source.toUpperCase() + '</span>'; - break; - } + source = escapeHtml(source); + var classes = ""; + switch (source.toLowerCase()) { + case 'stop': { + classes = 'label-danger'; + break; + } + case 'go': { + classes = 'label-success'; + break; + } + case 'warn': { + classes = 'label-warning'; + break; } } - return el; + return '<span class="label ' + classes + '">' + source.toUpperCase() + '</span>'; } function severity(source, type, val) { - var el = '<span>' + source + '</span>'; - if ((typeof source === 'string') && source.trim().toLowerCase().match(/(critical|high|medium|low|negligible|unknown)/g)) { - switch (source.trim().toLowerCase()) { - case 'critical': { - el = '<span style="display:none;">' + severityLookup[source.toLowerCase()] - + '</span><span class="label label-danger">' + source + '</span>'; - break; - } - case 'high': { - el = '<span style="display:none;">' + severityLookup[source.toLowerCase()] - + '</span><span class="label label-warning">' + source + '</span>'; - break; - } - case 'medium': { - el = '<span style = "display:none;">' + severityLookup[source.toLowerCase()] - + '</span><span class="label label-info">' + source + '</span>'; - break; - } - case 'low': { - el = '<span style="display:none;">' + severityLookup[source.toLowerCase()] - + '</span><span class="label label-success">' + source + '</span>'; - break; - } - case 'negligible': { - el = '<span style="display:none;">' + severityLookup[source.toLowerCase()] - + '</span><span class="label label-default">' + source + '</span>'; - break; - } - case 'unknown': { - el = '<span style = "display:none;">' + severityLookup[source.toLowerCase()] - + '</span><span class="label label-default">' + source + '</span>'; - break; - } + source = escapeHtml(source); + var classes = "label-default"; + switch (source.toLowerCase()) { + case 'critical': { + classes = 'label-danger'; + break; + } + case 'high': { + classes = 'label-warning'; + break; + } + case 'medium': { + classes = 'label-info'; + break; + } + case 'low': { + classes = 'label-success'; + break; } } - return el; + return '<span class="label ' + classes + '">' + source + '</span>'; } function buildPolicyEvalTable(tableId, outputFile) { @@ -101,9 +82,13 @@ function buildPolicyEvalTable(tableId, outputFile) { { targets: [0, 1, 2], render: function (source, type, val) { - return '<span style="word-break: break-all;">' + source + '</span>'; + return '<span style="word-break: break-all;">' + renderCell(source) + '</span>'; } }, + { + targets: [3, 4, 5, 7, 8], + render: renderCell + }, { targets: 6, render: gateAction @@ -129,47 +114,16 @@ function buildTableFromAnchoreOutput(tableId, outputFile) { jQuery.merge(rows, imageIdObj.result.rows); }); - jQuery(document).ready(function () { - jQuery(tableId).DataTable({ - retrieve: true, - data: rows, - columns: headers - }); - }); - }); -} - -function buildTableFromAnchoreOutputWithUrls(tableId, outputFile, index) { - var urlRegex = /(https?:\/\/[^\s\)]+)/g; - - jQuery.getJSON(outputFile, function (data) { - var headers = []; - var rows = []; - - jQuery.each(data, function (counter, imageIdObj) { - if (headers.length === 0) { - jQuery.each(imageIdObj.result.header, function (i, header) { - var headerObj = new Object(); - headerObj.title = header.replace('_', ' '); - headers.push(headerObj); - }); - } - - jQuery.merge(rows, imageIdObj.result.rows); - }); - jQuery(document).ready(function () { jQuery(tableId).DataTable({ retrieve: true, data: rows, columns: headers, columnDefs: [ { - render: function (data, type, row) { - return data.replace(urlRegex, '<a href="$1">$1</a>'); - }, - targets: index - } + targets: [0, 1, 2, 3, 4, 5], + render: renderCell + }, ] }); }); @@ -184,22 +138,26 @@ function buildPolicyEvalSummaryTable(tableId, tableObj) { columns: tableObj.header, order: [[4, 'asc']], columnDefs: [ + { + targets: 0, + render: renderCell + }, { targets: 1, render: function (source, type, val) { - return '<span class="label label-danger">' + source + '</span>'; + return '<span class="label label-danger">' + escapeHtml(source) + '</span>'; } }, { targets: 2, render: function (source, type, val) { - return '<span class="label label-warning">' + source + '</span>'; + return '<span class="label label-warning">' + escapeHtml(source) + '</span>'; } }, { targets: 3, render: function (source, type, val) { - return '<span class="label label-success">' + source + '</span>'; + return '<span class="label label-success">' + escapeHtml(source) + '</span>'; } }, { @@ -220,12 +178,55 @@ function buildSecurityTable(tableId, outputFile) { columns: tableObj.columns, order: [[2, 'asc'], [0, 'asc']], columnDefs: [ + { + targets: [0, 1, 3, 4], + render: renderCell + }, { targets: 2, render: severity + }, + { + targets: 5, + render: renderLinkCell } ] }); }); }); } + +const LINK_REGEX = /.*>(https?:\/\/)([^<]+)<.*/g; +const URL_REGEX = /(https?:\/\/[^\s)'"<]+)/g; + +function renderLinkCell(data) { + // originally links were embedded in some data, just extract the URL + if (LINK_REGEX.test(data)) { + data = data.replace(LINK_REGEX, "$1$2"); + } + return renderCell(data); +} + +function renderCell(data) { + if (typeof data == "string") { + data = escapeHtml(data) + return data.replace(URL_REGEX, '<a href="$1">$1</a>'); + } + return data; +} + +function escapeHtml(text) { + if (text === undefined || text === null) { + return ""; + } + if (typeof text !== "string") { + return "" + text; + } + return text + .trim() + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-f2j5-w76m-3rqhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-41225ghsaADVISORY
- github.com/jenkinsci/anchore-container-scanner-plugin/commit/1b1a62ab8ab86b409274e755860ab4e7fcc11800ghsaWEB
- www.jenkins.io/security/advisory/2022-09-21/ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.