VYPR
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.

PackageAffected versionsPatched versions
org.jenkins-ci.plugins:anchore-container-scannerMaven
< 1.0.251.0.25

Affected products

1

Patches

1
1b1a62ab8ab8

Updates 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, "&amp;")
    +      .replace(/</g, "&lt;")
    +      .replace(/>/g, "&gt;")
    +      .replace(/"/g, "&quot;")
    +      .replace(/'/g, "&#039;");
    +}
    

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

News mentions

0

No linked articles in our index yet.