VYPR
Moderate severityNVD Advisory· Published Mar 15, 2022· Updated Aug 3, 2024

CVE-2022-27197

CVE-2022-27197

Description

Jenkins Dashboard View Plugin 2.18 and earlier lacks URL validation in the Iframe Portlet, enabling stored XSS for attackers who can configure views.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Jenkins Dashboard View Plugin 2.18 and earlier lacks URL validation in the Iframe Portlet, enabling stored XSS for attackers who can configure views.

Vulnerability

Jenkins Dashboard View Plugin 2.18 and earlier does not perform URL validation for the Iframe Portlet's Iframe source URL [1] [3]. This allows a stored cross-site scripting (XSS) vulnerability. The affected versions are all releases up to and including 2.18 [1] [2].

Exploitation

An attacker must have the ability to configure views in Jenkins (e.g., View/Create or View/Configure permissions) [1]. In the view configuration, the attacker sets a malicious JavaScript payload as the Iframe source URL. When a user views the dashboard, the injected script executes in the context of the victim's session [1] [3].

Impact

Successful exploitation allows the attacker to execute arbitrary JavaScript in the browser of any user viewing the affected dashboard view. This can lead to information disclosure (e.g., Jenkins secrets, CSRF tokens), session hijacking, or arbitrary actions on the Jenkins controller with the victim's permissions [1].

Mitigation

Dashboard View Plugin 2.18.1, released on 2022-03-15, fixes the issue by properly validating the Iframe source URL [1] [2]. Users should upgrade to version 2.18.1 or later. There is no known workaround for unpatched versions [1]. The vulnerability is not currently listed on CISA's Known Exploited Vulnerabilities (KEV) catalog.

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.

PackageAffected versionsPatched versions
org.jenkins-ci.plugins:dashboard-viewMaven
< 2.18.12.18.1

Affected products

2

Patches

1
942c5c78fa83

[SECURITY-2559] Stored XSS vulnerability iframeportlet

4 files changed · +190 7
  • src/main/java/hudson/plugins/view/dashboard/core/IframePortlet.java+57 2 modified
    @@ -1,23 +1,34 @@
     package hudson.plugins.view.dashboard.core;
     
    +import edu.umd.cs.findbugs.annotations.CheckForNull;
     import hudson.Extension;
     import hudson.model.Descriptor;
     import hudson.model.Job;
     import hudson.plugins.view.dashboard.DashboardPortlet;
     import hudson.plugins.view.dashboard.Messages;
    +import hudson.util.FormValidation;
    +import java.net.URI;
    +import java.net.URISyntaxException;
     import java.util.Iterator;
    +import jenkins.util.SystemProperties;
    +import org.apache.commons.lang.StringUtils;
     import org.kohsuke.stapler.DataBoundConstructor;
    +import org.kohsuke.stapler.DataBoundSetter;
    +import org.kohsuke.stapler.QueryParameter;
     
     public class IframePortlet extends DashboardPortlet {
     
       private String iframeSource;
       private String effectiveUrl;
       private final String divStyle;
    +  private static boolean DO_NOT_USE_SANDBOX =
    +      SystemProperties.getBoolean(IframePortlet.class.getName() + ".doNotUseSandbox");
    +  private static String SANDBOX_VALUE =
    +      SystemProperties.getString(IframePortlet.class.getName() + ".sandboxAttributeValue", "");
     
       @DataBoundConstructor
    -  public IframePortlet(String name, String iframeSource, String divStyle) {
    +  public IframePortlet(String name, String divStyle) {
         super(name);
    -    this.setIframeSource(iframeSource);
         this.divStyle = divStyle;
       }
     
    @@ -33,11 +44,24 @@ public String getDivStyle() {
         return divStyle;
       }
     
    +  @DataBoundSetter
       public void setIframeSource(String iframeSource) {
         this.iframeSource = iframeSource;
         this.overridePlaceholdersInUrl();
       }
     
    +  public boolean isIframeSourceValid() {
    +    return getUrlError(iframeSource) == null;
    +  }
    +
    +  public boolean isUseSandbox() {
    +    return !DO_NOT_USE_SANDBOX;
    +  }
    +
    +  public String getSandboxValue() {
    +    return SANDBOX_VALUE;
    +  }
    +
       private void overridePlaceholdersInUrl() {
         if (iframeSource != null) {
           if (getDashboard() != null) {
    @@ -71,5 +95,36 @@ public static class DescriptorImpl extends Descriptor<DashboardPortlet> {
         public String getDisplayName() {
           return Messages.Dashboard_IframePortlet();
         }
    +
    +    public FormValidation doCheckIframeSource(@QueryParameter String value) {
    +      String error = getUrlError(value);
    +      if (error != null) {
    +        return FormValidation.error(error);
    +      }
    +      return FormValidation.ok();
    +    }
    +  }
    +
    +  /**
    +   * Checks if the passed string is a valid HTTP or relative URL.
    +   *
    +   * @return Localized error message or null if URL is valid.
    +   */
    +  @CheckForNull
    +  protected static final String getUrlError(String url) {
    +    if (StringUtils.isBlank(url)) {
    +      return Messages.Dashboard_UrlEmpty();
    +    }
    +    try {
    +      final URI allowedUrl = new URI(url);
    +      final String protocol = allowedUrl.getScheme();
    +      if (!allowedUrl.isAbsolute() || protocol.equals("http") || protocol.equals("https")) {
    +        return null;
    +      } else {
    +        return Messages.Dashboard_UrlHttp();
    +      }
    +    } catch (URISyntaxException e) {
    +      return Messages.Dashboard_UrlInvalid(e.getMessage());
    +    }
       }
     }
    
  • src/main/resources/hudson/plugins/view/dashboard/core/IframePortlet/portlet.jelly+21 5 modified
    @@ -26,11 +26,27 @@ THE SOFTWARE.
     <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:dp="/hudson/plugins/view/dashboard" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
       <dp:decorate portlet="${it}">
       	<tr><td>
    -  		<div align="center" style="${it.divStyle}">
    -	    	<iframe src="${it.effectiveUrl}" style="width:100%;height:100%;border:0;">
    -    	    	  Your browser doesn't support iframes!
    -    		</iframe>
    -    	</div>
    +      <j:choose>
    +        <j:when test="${it.iframeSourceValid}">
    +          <div align="center" style="${it.divStyle}">
    +            <j:choose>
    +              <j:when test="${it.useSandbox}">
    +                <iframe src="${it.effectiveUrl}" style="width:100%;height:100%;border:0;" sandbox="${it.sandboxValue}">
    +                    Your browser doesn't support iframes!
    +                </iframe>
    +              </j:when>
    +              <j:otherwise>
    +                <iframe src="${it.effectiveUrl}" style="width:100%;height:100%;border:0;">
    +                    Your browser doesn't support iframes!
    +                </iframe>
    +              </j:otherwise>
    +            </j:choose>
    +          </div>
    +        </j:when>
    +        <j:otherwise>
    +          <div class="error">${%Display error: URL is invalid}</div>
    +        </j:otherwise>
    +      </j:choose>
         </td></tr>
       </dp:decorate>
     </j:jelly>
    
  • src/main/resources/hudson/plugins/view/dashboard/Messages.properties+3 0 modified
    @@ -25,3 +25,6 @@ Dashboard.ImageUrlEmpty=Image URL should not be empty.
     Dashboard.ImageUrlInvalid=Image URL is invalid: {0}
     Dashboard.ImageUrlHttp=Image URL must be http or https.
     Dashboard.IframePortlet=Iframe Portlet
    +Dashboard.UrlEmpty=URL should not be empty.
    +Dashboard.UrlInvalid=URL is invalid: {0}
    +Dashboard.UrlHttp=URL must be http or https.
    
  • src/test/java/hudson/plugins/view/dashboard/core/IFramePortletTest.java+109 0 added
    @@ -0,0 +1,109 @@
    +package hudson.plugins.view.dashboard.core;
    +
    +import static org.hamcrest.MatcherAssert.assertThat;
    +import static org.hamcrest.Matchers.emptyIterable;
    +import static org.hamcrest.Matchers.emptyString;
    +import static org.hamcrest.Matchers.hasSize;
    +import static org.hamcrest.Matchers.is;
    +import static org.hamcrest.Matchers.notNullValue;
    +import static org.hamcrest.Matchers.nullValue;
    +
    +import com.gargoylesoftware.htmlunit.html.DomNode;
    +import com.gargoylesoftware.htmlunit.html.HtmlInlineFrame;
    +import com.gargoylesoftware.htmlunit.html.HtmlPage;
    +import hudson.plugins.view.dashboard.Dashboard;
    +import java.util.Arrays;
    +import java.util.List;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.jvnet.hudson.test.Issue;
    +import org.jvnet.hudson.test.JenkinsRule;
    +import org.jvnet.hudson.test.WithoutJenkins;
    +
    +public class IFramePortletTest {
    +  @Rule public JenkinsRule j = new JenkinsRule();
    +
    +  @Test
    +  @Issue("SECURITY-2559")
    +  public void iframePortletValidation() throws Exception {
    +    j.createFreeStyleProject("bar");
    +
    +    Dashboard dashboard = new Dashboard("dash1");
    +    dashboard.setIncludeRegex(".*");
    +    j.jenkins.addView(dashboard);
    +
    +    List<String> invalidUrls =
    +        Arrays.asList(
    +            "", "<img/src/onerror=alert(\"XSS\")>", "ftp://foo.com", "javascript:alert(1)");
    +
    +    for (String invalid : invalidUrls) {
    +      IframePortlet iframePortlet = new IframePortlet("bar", "");
    +      iframePortlet.setIframeSource(invalid);
    +      dashboard.getBottomPortlets().clear();
    +      dashboard.getBottomPortlets().add(iframePortlet);
    +      assertThat(iframePortlet.isIframeSourceValid(), is(false));
    +      HtmlPage page = j.createWebClient().goTo("view/dash1/");
    +      assertThat(findError(page), hasSize(1));
    +      assertThat(findIFrame(page), is(emptyIterable()));
    +    }
    +
    +    List<String> validUrls =
    +        Arrays.asList(j.getURL().toString(), j.getURL().toString() + "/job/bar");
    +
    +    for (String valid : validUrls) {
    +      IframePortlet iframePortlet = new IframePortlet("bar", valid);
    +      iframePortlet.setIframeSource(valid);
    +      dashboard.getBottomPortlets().clear();
    +      dashboard.getBottomPortlets().add(iframePortlet);
    +      assertThat(valid + " is not valid", iframePortlet.isIframeSourceValid(), is(true));
    +      HtmlPage page = j.createWebClient().goTo("view/dash1/");
    +      assertThat(findError(page), is(emptyIterable()));
    +      assertThat(findIFrame(page), hasSize(1));
    +    }
    +  }
    +
    +  @WithoutJenkins
    +  public void validateUri() throws Exception {
    +    assertThat(IframePortlet.getUrlError("https://internal_host.example.com/foo"), nullValue());
    +    assertThat(IframePortlet.getUrlError("//internal_host.example.com/foo"), nullValue());
    +    assertThat(IframePortlet.getUrlError("file://internal_host.example.com/foo"), notNullValue());
    +    assertThat(IframePortlet.getUrlError("ftp://internal_host.example.com/foo"), notNullValue());
    +    assertThat(IframePortlet.getUrlError("//Users/foo/bar/beer"), nullValue());
    +    assertThat(IframePortlet.getUrlError("ssh://internal_host.example.com/foo"), notNullValue());
    +    assertThat(IframePortlet.getUrlError("<img/src/onerror=alert(\"XSS\")>"), notNullValue());
    +    assertThat(IframePortlet.getUrlError("javascript:alert(1)"), notNullValue());
    +  }
    +
    +  @Test
    +  @Issue("SECURITY-2565")
    +  public void iframePortletSandbox() throws Exception {
    +    j.createFreeStyleProject("bar");
    +
    +    Dashboard dashboard = new Dashboard("dash2");
    +    dashboard.setIncludeRegex(".*");
    +    j.jenkins.addView(dashboard);
    +
    +    for (String valid : Arrays.asList(j.getURL().toString(), j.getURL().toString() + "/job/bar")) {
    +      IframePortlet iframePortlet = new IframePortlet("bar", valid);
    +      iframePortlet.setIframeSource(valid);
    +      dashboard.getBottomPortlets().clear();
    +      dashboard.getBottomPortlets().add(iframePortlet);
    +      assertThat(valid + " is not valid", iframePortlet.isIframeSourceValid(), is(true));
    +      HtmlPage page = j.createWebClient().goTo("view/dash2/");
    +      assertThat(findError(page), is(emptyIterable()));
    +      assertThat(findIFrame(page), hasSize(1));
    +      HtmlInlineFrame frameWindow = findIFrame(page).get(0);
    +      String sanboxValue = frameWindow.getAttribute("sandbox");
    +      assertThat("sandbox attribute not found", sanboxValue, notNullValue());
    +      assertThat("sandbox attribute not empty", sanboxValue, emptyString());
    +    }
    +  }
    +
    +  private List<HtmlInlineFrame> findIFrame(HtmlPage page) {
    +    return page.getByXPath("//table[@id='portlet-bottomPortlets-0']//iframe");
    +  }
    +
    +  private List<DomNode> findError(HtmlPage page) {
    +    return page.getByXPath("//div[@class='error']");
    +  }
    +}
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

1