CVE-2022-28145
Description
Jenkins Continuous Integration with Toad Edge Plugin ≤2.3 lacks Content-Security-Policy headers on report files, enabling stored XSS for attackers with Item/Configure permission.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins Continuous Integration with Toad Edge Plugin ≤2.3 lacks Content-Security-Policy headers on report files, enabling stored XSS for attackers with Item/Configure permission.
Vulnerability
Jenkins Continuous Integration with Toad Edge Plugin version 2.3 and earlier does not apply Content-Security-Policy (CSP) headers to report files it serves [1][3]. This allows an attacker to inject malicious scripts into report content, leading to stored cross-site scripting (XSS). The vulnerability affects all versions up to and including 2.3.
Exploitation
An attacker must have Item/Configure permission on a Jenkins job or otherwise be able to control the contents of report files [1][3]. The attacker can craft a report containing malicious JavaScript. When a user views the report, the script executes in the context of the Jenkins session.
Impact
Successful exploitation results in stored XSS, allowing the attacker to execute arbitrary JavaScript in the victim's browser. This can lead to information disclosure, session hijacking, or other actions within the Jenkins interface at the victim's privilege level.
Mitigation
The fix was released in Continuous Integration with Toad Edge Plugin version 2.4 on March 29, 2022 [2]. Users should upgrade to version 2.4 or later. No workarounds are documented; upgrading is the recommended mitigation.
AI Insight generated on May 21, 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:ci-with-toad-edgeMaven | < 2.4 | 2.4 |
Affected products
2- Jenkins project/Jenkins Continuous Integration with Toad Edge Pluginv5Range: unspecified
Patches
1d6b2292949d0{SECURITY-1892}
3 files changed · +17 −641
README.adoc+16 −0 modified@@ -74,3 +74,19 @@ http://support.quest.com/technical-documents/toad-edge/2.1/user-guide/continuous https://support.quest.com/technical-documents/toad-edge/2.2/user-guide/continuous-integration-and-delivery |=== + +[[ContinuousIntegrationWithToadEdgePlugin-Documentation]] +== Troubleshooting +If you are having issues viewing Jenkins HTML comparison report, it could be due to your browser's Content Security Policy. You can check your browser console to confirm the same. +Also, the reports will not get rendered if Jenkins's https://www.jenkins.io/doc/book/security/user-content/#resource-root-url[resource root url] is configured. + +To view Jenkins HTML reports, you would have to relax the CSP. This can be done by going to _Jenkins > Manage Jenkins > Script Console_ + +*Execute this script to relax CSP:* + +`System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "default-src 'self'; script-src * 'self' 'unsafe-inline'; img-src 'self'; style-src 'self' 'unsafe-inline'; font-src * data:");` + +This will be reverted once Jenkins restarts + +_Please note that we would also suggest you to review https://www.jenkins.io/doc/book/security/configuring-content-security-policy/ and https://content-security-policy.com/ to understand the nature of CSP and the protection which it offers before changing CSP._ +
src/main/java/ci/with/toad/edge/DirectoryBrowserSupport.java+0 −641 removed@@ -1,641 +0,0 @@ -/* - * Copyright 2021 Quest Software Inc. - * ALL RIGHTS RESERVED. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressor implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ci.with.toad.edge; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Serializable; -import java.text.Collator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; -import java.util.StringTokenizer; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.io.IOUtils; -import org.apache.tools.zip.ZipEntry; -import org.apache.tools.zip.ZipOutputStream; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; -import hudson.FilePath; -import hudson.Util; -import hudson.model.ModelObject; -import jenkins.model.Jenkins; -import jenkins.security.MasterToSlaveCallable; -import jenkins.util.VirtualFile; - -/** - * Has convenience methods to serve file system. - * - * <p> - * This object can be used in a mix-in style to provide a directory browsing - * capability to a {@link ModelObject}. - * - * @author Kohsuke Kawaguchi - */ -public final class DirectoryBrowserSupport implements HttpResponse { - - public final ModelObject owner; - - public final String title; - - private final VirtualFile base; - private final String icon; - private final boolean serveDirIndex; - private String indexFileName = "index.html"; - - /** - * @param owner The parent model object under which the directory browsing is added. - * @param title Used in the HTML caption. - * - * @deprecated as of 1.297 Use - * {@link #DirectoryBrowserSupport(ModelObject, FilePath, String, String, boolean)} - */ - @Deprecated - public DirectoryBrowserSupport(ModelObject owner, String title) { - this(owner, (VirtualFile) null, title, null, false); - } - - /** - * @param owner - * The parent model object under which the directory browsing is - * added. - * @param base - * The root of the directory that's bound to URL. - * @param title - * Used in the HTML caption. - * @param icon - * The icon file name, like "folder.gif" - * @param serveDirIndex - * True to generate the directory index. False to serve - * "index.html" - */ - public DirectoryBrowserSupport(ModelObject owner, FilePath base, String title, String icon, boolean serveDirIndex) { - this(owner, base.toVirtualFile(), title, icon, serveDirIndex); - } - - /** - * @param owner - * The parent model object under which the directory browsing is - * added. - * @param base - * The root of the directory that's bound to URL. - * @param title - * Used in the HTML caption. - * @param icon - * The icon file name, like "folder.gif" - * @param serveDirIndex - * True to generate the directory index. False to serve - * "index.html" - * @since 1.532 - */ - public DirectoryBrowserSupport(ModelObject owner, VirtualFile base, String title, String icon, - boolean serveDirIndex) { - this.owner = owner; - this.base = base; - this.title = title; - this.icon = icon; - this.serveDirIndex = serveDirIndex; - } - - public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) - throws IOException, ServletException { - try { - serveFile(req, rsp, base, icon, serveDirIndex); - } catch (InterruptedException e) { - throw new IOException("interrupted", e); - } - } - - /** - * If the directory is requested but the directory listing is disabled, a - * file of this name is served. By default it's "index.html". - * - * @param fileName - * - name of the file to use as index - * - * @since 1.312 - */ - public void setIndexFileName(String fileName) { - this.indexFileName = fileName; - } - - /** - * Serves a file from the file system (Maps the URL to a directory in a file - * system.) - * - * @param req - * Request - * @param rsp - * Response - * @param root - * Server root - * @param icon - * The icon file name, like "folder-open.gif" - * @param serveDirIndex - * True to generate the directory index. False to serve - * "index.html" - * - * @throws IOException IOException - * @throws ServletException ServletException - * @throws InterruptedException InterruptedException - * - * @deprecated as of 1.297 Instead of calling this method explicitly, just - * return the {@link DirectoryBrowserSupport} object from the - * {@code doXYZ} method and let Stapler generate a response for - * you. - */ - @Deprecated - public void serveFile(StaplerRequest req, StaplerResponse rsp, FilePath root, String icon, boolean serveDirIndex) - throws IOException, ServletException, InterruptedException { - serveFile(req, rsp, root.toVirtualFile(), icon, serveDirIndex); - } - - private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root, String icon, - boolean serveDirIndex) throws IOException, ServletException, InterruptedException { - // handle form submission - String pattern = req.getParameter("pattern"); - if (pattern == null) - pattern = req.getParameter("path"); // compatibility with - // Hudson<1.129 - if (pattern != null && !Util.isAbsoluteUri(pattern)) {// avoid open - // redirect - rsp.sendRedirect2(pattern); - return; - } - - String path = getPath(req); - if (path.replace('\\', '/').indexOf("/../") != -1) { - // don't serve anything other than files in the artifacts dir - rsp.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - - // split the path to the base directory portion "abc/def/ghi" which - // doesn't include any wildcard, - // and the GLOB portion "**/*.xml" (the rest) - StringBuilder _base = new StringBuilder(); - StringBuilder _rest = new StringBuilder(); - int restSize = -1; // number of ".." needed to go back to the 'base' - // level. - boolean zip = false; // if we are asked to serve a zip file bundle - boolean plain = false; // if asked to serve a plain text directory - // listing - { - boolean inBase = true; - StringTokenizer pathTokens = new StringTokenizer(path, "/"); - while (pathTokens.hasMoreTokens()) { - String pathElement = pathTokens.nextToken(); - // Treat * and ? as wildcard unless they match a literal - // filename - if ((pathElement.contains("?") || pathElement.contains("*")) && inBase - && !root.child((_base.length() > 0 ? _base + "/" : "") + pathElement).exists()) - inBase = false; - if (pathElement.equals("*zip*")) { - // the expected syntax is foo/bar/*zip*/bar.zip - // the last 'bar.zip' portion is to causes browses to set a - // good default file name. - // so the 'rest' portion ends here. - zip = true; - break; - } - if (pathElement.equals("*plain*")) { - plain = true; - break; - } - - StringBuilder sb = inBase ? _base : _rest; - if (sb.length() > 0) - sb.append('/'); - sb.append(pathElement); - if (!inBase) - restSize++; - } - } - restSize = Math.max(restSize, 0); - String base = _base.toString(); - String rest = _rest.toString(); - - // this is the base file/directory - VirtualFile baseFile = root.child(base); - - if (baseFile.isDirectory()) { - if (zip) { - rsp.setContentType("application/zip"); - zip(rsp.getOutputStream(), baseFile, rest); - return; - } - if (plain) { - rsp.setContentType("text/plain;charset=UTF-8"); - OutputStream os = rsp.getOutputStream(); - try { - for (VirtualFile kid : baseFile.list()) { - os.write(kid.getName().getBytes("UTF-8")); - if (kid.isDirectory()) { - os.write('/'); - } - os.write('\n'); - } - os.flush(); - } finally { - os.close(); - } - return; - } - - if (rest.length() == 0) { - // if the target page to be displayed is a directory and the - // path doesn't end with '/', redirect - StringBuffer reqUrl = req.getRequestURL(); - if (reqUrl.charAt(reqUrl.length() - 1) != '/') { - rsp.sendRedirect2(reqUrl.append('/').toString()); - return; - } - } - - List<List<Path>> glob = null; - - if (rest.length() > 0) { - // the rest is Ant glob pattern - glob = patternScan(baseFile, rest, createBackRef(restSize)); - } else if (serveDirIndex) { - // serve directory index - glob = baseFile.run(new BuildChildPaths(baseFile, req.getLocale())); - } - - if (glob != null) { - // serve glob - req.setAttribute("it", this); - List<Path> parentPaths = buildParentPath(base, restSize); - req.setAttribute("parentPath", parentPaths); - req.setAttribute("backPath", createBackRef(restSize)); - req.setAttribute("topPath", createBackRef(parentPaths.size() + restSize)); - req.setAttribute("files", glob); - req.setAttribute("icon", icon); - req.setAttribute("path", path); - req.setAttribute("pattern", rest); - req.setAttribute("dir", baseFile); - req.getView(this, "dir.jelly").forward(req, rsp); - return; - } - - // convert a directory service request to a single file service - // request by serving - // 'index.html' - baseFile = baseFile.child(indexFileName); - } - - // serve a single file - if (!baseFile.exists()) { - rsp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - boolean view = rest.equals("*view*"); - - if (rest.equals("*fingerprint*")) { - InputStream fingerprintInput = baseFile.open(); - try { - Jenkins jenkins = Jenkins.getInstance(); - if (jenkins != null) { - rsp.forward(jenkins.getFingerprint(Util.getDigestOf(fingerprintInput)), "/", req); - } - } finally { - fingerprintInput.close(); - } - return; - } - - long lastModified = baseFile.lastModified(); - long length = baseFile.length(); - - if (LOGGER.isLoggable(Level.FINE)) - LOGGER.fine("Serving " + baseFile + " with lastModified=" + lastModified + ", length=" + length); - - InputStream in = baseFile.open(); - if (view) { - // for binary files, provide the file name for download - rsp.setHeader("Content-Disposition", "inline; filename=" + baseFile.getName()); - - // pseudo file name to let the Stapler set text/plain - rsp.serveFile(req, in, lastModified, -1, length, "plain.txt"); - } else { - rsp.serveFile(req, in, lastModified, -1, length, baseFile.getName()); - } - } - - private String getPath(StaplerRequest req) { - String path = req.getRestOfPath(); - if (path.length() == 0) - path = "/"; - return path; - } - - /** - * Builds a list of {@link Path} that represents ancestors from a string - * like "/foo/bar/zot". - */ - private List<Path> buildParentPath(String pathList, int restSize) { - List<Path> r = new ArrayList<Path>(); - StringTokenizer tokens = new StringTokenizer(pathList, "/"); - int total = tokens.countTokens(); - int current = 1; - while (tokens.hasMoreTokens()) { - String token = tokens.nextToken(); - r.add(new Path(createBackRef(total - current + restSize), token, true, 0, true)); - current++; - } - return r; - } - - private static String createBackRef(int times) { - if (times == 0) - return "./"; - StringBuilder buf = new StringBuilder(3 * times); - for (int i = 0; i < times; i++) - buf.append("../"); - return buf.toString(); - } - - private static void zip(OutputStream outputStream, VirtualFile dir, String glob) throws IOException { - ZipOutputStream zos = new ZipOutputStream(outputStream); - zos.setEncoding(System.getProperty("file.encoding")); // TODO - // JENKINS-20663 - // make this - // overridable - // via query - // parameter - for (String n : dir.list(glob.length() == 0 ? "**" : glob)) { - String relativePath; - if (glob.length() == 0) { - // JENKINS-19947: traditional behavior is to prepend the - // directory name - relativePath = dir.getName() + '/' + n; - } else { - relativePath = n; - } - // In ZIP archives "All slashes MUST be forward slashes" - // (http://pkware.com/documents/casestudies/APPNOTE.TXT) - // TODO On Linux file names can contain backslashes which should not - // treated as file separators. - // Unfortunately, only the file separator char of the master is - // known (File.separatorChar) - // but not the file separator char of the (maybe remote) "dir". - ZipEntry e = new ZipEntry(relativePath.replace('\\', '/')); - VirtualFile f = dir.child(n); - e.setTime(f.lastModified()); - zos.putNextEntry(e); - InputStream in = f.open(); - try { - Util.copyStream(in, zos); - } finally { - IOUtils.closeQuietly(in); - } - zos.closeEntry(); - } - zos.close(); - } - - /** - * Represents information about one file or folder. - */ - public static final class Path implements Serializable { - /** - * Relative URL to this path from the current page. - */ - private final String href; - /** - * Name of this path. Just the file name portion. - */ - private final String title; - - private final boolean isFolder; - - /** - * File size, or null if this is not a file. - */ - private final long size; - - /** - * If the current user can read the file. - */ - private final boolean isReadable; - - public Path(String href, String title, boolean isFolder, long size, boolean isReadable) { - this.href = href; - this.title = title; - this.isFolder = isFolder; - this.size = size; - this.isReadable = isReadable; - } - - public boolean isFolder() { - return isFolder; - } - - public boolean isReadable() { - return isReadable; - } - - public String getHref() { - return href; - } - - public String getTitle() { - return title; - } - - public String getIconName() { - if (isReadable) - return isFolder ? "folder.png" : "text.png"; - else - return isFolder ? "folder-error.png" : "text-error.png"; - } - - public String getIconClassName() { - if (isReadable) - return isFolder ? "icon-folder" : "icon-text"; - else - return isFolder ? "icon-folder-error" : "icon-text-error"; - } - - public long getSize() { - return size; - } - - private static final long serialVersionUID = 1L; - } - - private static final class FileComparator implements Comparator<VirtualFile> { - private Collator collator; - - FileComparator(Locale locale) { - this.collator = Collator.getInstance(locale); - } - - public int compare(VirtualFile lhs, VirtualFile rhs) { - // directories first, files next - int r = dirRank(lhs) - dirRank(rhs); - if (r != 0) - return r; - // otherwise alphabetical - return this.collator.compare(lhs.getName(), rhs.getName()); - } - - private int dirRank(VirtualFile f) { - try { - if (f.isDirectory()) - return 0; - else - return 1; - } catch (IOException ex) { - return 0; - } - } - } - - private static final class BuildChildPaths extends MasterToSlaveCallable<List<List<Path>>, IOException> { - private final VirtualFile cur; - private final Locale locale; - - BuildChildPaths(VirtualFile cur, Locale locale) { - this.cur = cur; - this.locale = locale; - } - - @Override - public List<List<Path>> call() throws IOException { - return buildChildPaths(cur, locale); - } - } - - /** - * Builds a list of list of {@link Path}. The inner list of {@link Path} - * represents one child item to be shown (this mechanism is used to skip - * empty intermediate directory.) - */ - private static List<List<Path>> buildChildPaths(VirtualFile cur, Locale locale) throws IOException { - List<List<Path>> r = new ArrayList<List<Path>>(); - - VirtualFile[] files = cur.list(); - Arrays.sort(files, new FileComparator(locale)); - - for (VirtualFile f : files) { - Path p = new Path(Util.rawEncode(f.getName()), f.getName(), f.isDirectory(), f.length(), f.canRead()); - if (!f.isDirectory()) { - r.add(Collections.singletonList(p)); - } else { - // find all empty intermediate directory - List<Path> l = new ArrayList<Path>(); - l.add(p); - StringBuilder relPath = new StringBuilder(Util.rawEncode(f.getName())); - while (true) { - // files that don't start with '.' qualify for 'meaningful - // files', nor SCM related files - List<VirtualFile> sub = new ArrayList<VirtualFile>(); - for (VirtualFile vf : f.list()) { - String name = vf.getName(); - if (!name.startsWith(".") && !name.equals("CVS") && !name.equals(".svn")) { - sub.add(vf); - } - } - if (sub.size() != 1 || !sub.get(0).isDirectory()) - break; - f = sub.get(0); - relPath.append('/' + Util.rawEncode(f.getName())); - l.add(new Path(relPath.toString(), f.getName(), true, 0, f.canRead())); - } - r.add(l); - } - } - - return r; - } - - /** - * Runs ant GLOB against the current {@link FilePath} and returns matching - * paths. - * - * @param baseRef - * String like "../../../" that cancels the 'rest' portion. Can - * be "./" - */ - private static List<List<Path>> patternScan(VirtualFile baseDir, String pattern, String baseRef) - throws IOException { - String[] files = baseDir.list(pattern); - - if (files.length > 0) { - List<List<Path>> r = new ArrayList<List<Path>>(files.length); - for (String match : files) { - List<Path> file = buildPathList(baseDir, baseDir.child(match), baseRef); - r.add(file); - } - return r; - } - - return null; - } - - /** - * Builds a path list from the current workspace directory down to the - * specified file path. - */ - private static List<Path> buildPathList(VirtualFile baseDir, VirtualFile filePath, String baseRef) - throws IOException { - List<Path> pathList = new ArrayList<Path>(); - StringBuilder href = new StringBuilder(baseRef); - - buildPathList(baseDir, filePath, pathList, href); - return pathList; - } - - /** - * Builds the path list and href recursively top-down. - */ - private static void buildPathList(VirtualFile baseDir, VirtualFile filePath, List<Path> pathList, - StringBuilder href) throws IOException { - VirtualFile parent = filePath.getParent(); - if (!baseDir.equals(parent)) { - buildPathList(baseDir, parent, pathList, href); - } - - href.append(Util.rawEncode(filePath.getName())); - if (filePath.isDirectory()) { - href.append("/"); - } - - Path path = new Path(href.toString(), filePath.getName(), filePath.isDirectory(), filePath.length(), - filePath.canRead()); - pathList.add(path); - } - - private static final Logger LOGGER = Logger.getLogger(DirectoryBrowserSupport.class.getName()); - - @Restricted(NoExternalUse.class) - public static final String DEFAULT_CSP_VALUE = "sandbox; default-src 'none'; img-src 'self'; style-src 'self';"; -}
src/main/java/ci/with/toad/edge/ReportPublisherAction.java+1 −0 modified@@ -27,6 +27,7 @@ import hudson.FilePath; import hudson.model.Action; +import hudson.model.DirectoryBrowserSupport; import hudson.model.Run; import jenkins.model.RunAction2;
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-7jh8-ghwc-82cwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-28145ghsaADVISORY
- www.openwall.com/lists/oss-security/2022/03/29/1ghsamailing-listx_refsource_MLISTWEB
- github.com/jenkinsci/ci-with-toad-edge-plugin/commit/d6b2292949d0cddf4a2981fefbf3e30d1dfcd88aghsaWEB
- www.jenkins.io/security/advisory/2022-03-29/ghsax_refsource_CONFIRMWEB
News mentions
1- Jenkins Security Advisory 2022-03-29Jenkins Security Advisories · Mar 29, 2022