Stored XSS vulnerability in Jenkins upstream cause
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
0 → fixed in 2.235.22.236 → fixed in 2.245
Details
Jenkins 2.244 and earlier, LTS 2.235.1 and earlier does not escape the upstream job's display name shown as part of a build cause, resulting in a stored cross-site scripting vulnerability. Jenkins 2.245, LTS 2.235.2 escapes the job display name.
The fix
[SECURITY-1901]
core/src/main/resources/hudson/model/Cause/UpstreamCause/description.jelly+4 −4
@@ -29,20 +29,20 @@ THE SOFTWARE.<j:when test="${it.upstreamUrl!=null and app.getItemByFullName(it.upstreamProject)!=null}"><j:choose><j:when test="${app.getItemByFullName(it.upstreamProject).getBuildByNumber(it.upstreamBuild)!=null}">-<j:out value="${%started_by_project(app.getItemByFullName(it.upstreamProject).fullDisplayName,it.upstreamBuild.toString(),it.upstreamUrl,rootURL)}"/>+${%started_by_project(app.getItemByFullName(it.upstreamProject).fullDisplayName,it.upstreamBuild.toString(),it.upstreamUrl,rootURL)}</j:when><j:otherwise>-<j:out value="${%started_by_project_with_deleted_build(app.getItemByFullName(it.upstreamProject).fullDisplayName,it.upstreamBuild.toString(),it.upstreamUrl,rootURL)}"/>+${%started_by_project_with_deleted_build(app.getItemByFullName(it.upstreamProject).fullDisplayName,it.upstreamBuild.toString(),it.upstreamUrl,rootURL)}</j:otherwise></j:choose></j:when><j:otherwise>-<j:out value="${it.shortDescription}"/>+${it.shortDescription}</j:otherwise></j:choose><j:if test="${!it.upstreamCauses.isEmpty()}">-<br /><j:out value="${%caused_by}" />+<br />${%caused_by}<ul><j:forEach var="c" items="${it.upstreamCauses}"><li><st:include page="description.jelly" it="${c}" /></li>
test/src/test/java/hudson/model/UpstreamCauseSEC1901Test.java+167 −0
@@ -0,0 +1,167 @@+/*+* The MIT License+*+* Copyright (c) 2020 CloudBees, Inc.+*+* Permission is hereby granted, free of charge, to any person obtaining a copy+* of this software and associated documentation files (the "Software"), to deal+* in the Software without restriction, including without limitation the rights+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+* copies of the Software, and to permit persons to whom the Software is+* furnished to do so, subject to the following conditions:+*+* The above copyright notice and this permission notice shall be included in+* all copies or substantial portions of the Software.+*+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+* THE SOFTWARE.+*/+package hudson.model;++import hudson.tasks.BuildTrigger;+import jenkins.model.Jenkins;+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.TestExtension;++import java.io.IOException;+import java.util.concurrent.atomic.AtomicBoolean;++import static org.junit.Assert.assertFalse;++public class UpstreamCauseSEC1901Test {++@Rule+public JenkinsRule j = new JenkinsRule();++@Test+@Issue("SECURITY-1901")+public void preventXssInUpstreamDisplayName() throws Exception {+j.jenkins.setQuietPeriod(0);+FreeStyleProject up = j.createFreeStyleProject("up");+up.setDisplayName("Up<script>alert(123)</script>Project");++FreeStyleProject down = j.createFreeStyleProject("down");++up.getPublishersList().add(new BuildTrigger(down.getFullName(), false));++j.jenkins.rebuildDependencyGraph();++j.buildAndAssertSuccess(up);++FreeStyleBuild downBuild = this.waitForDownBuild(down);++ensureXssIsPrevented(downBuild);+}++@Test+@Issue("SECURITY-1901")+public void preventXssInUpstreamDisplayName_deleted() throws Exception {+j.jenkins.setQuietPeriod(0);+FreeStyleProject up = j.createFreeStyleProject("up");+up.setDisplayName("Up<script>alert(123)</script>Project");++FreeStyleProject down = j.createFreeStyleProject("down");++up.getPublishersList().add(new BuildTrigger(down.getFullName(), false));++j.jenkins.rebuildDependencyGraph();++FreeStyleBuild upBuild = j.buildAndAssertSuccess(up);++FreeStyleBuild downBuild = this.waitForDownBuild(down);++// that will display a different part+upBuild.delete();++ensureXssIsPrevented(downBuild);+}++@Test+@Issue("SECURITY-1901")+public void preventXssInUpstreamShortDescription() throws Exception {+FullNameChangingProject up = j.createProject(FullNameChangingProject.class, "up");++FreeStyleProject down = j.createFreeStyleProject("down");++CustomBuild upBuild = j.buildAndAssertSuccess(up);++up.setVirtualName("Up<script>alert(123)</script>Project");+j.assertBuildStatusSuccess(down.scheduleBuild2(0, new Cause.UpstreamCause(upBuild)));+up.setVirtualName(null);++FreeStyleBuild downBuild = this.waitForDownBuild(down);++ensureXssIsPrevented(downBuild);+}++private void ensureXssIsPrevented(FreeStyleBuild downBuild) throws Exception {+AtomicBoolean alertCalled = new AtomicBoolean(false);++JenkinsRule.WebClient wc = j.createWebClient();+wc.setAlertHandler((page, s) -> alertCalled.set(true));+wc.goTo(downBuild.getUrl());++assertFalse("XSS not prevented", alertCalled.get());+}++private <B extends Build<?, B>> B waitForDownBuild(Project<?, B> down) throws Exception {+j.waitUntilNoActivity();+B result = down.getBuilds().getLastBuild();++return result;+}++public static class CustomBuild extends Build<FullNameChangingProject, CustomBuild> {+public CustomBuild(FullNameChangingProject job) throws IOException {+super(job);+}+}++static class FullNameChangingProject extends Project<FullNameChangingProject, CustomBuild> implements TopLevelItem {+private volatile String virtualName;++public FullNameChangingProject(ItemGroup parent, String name) {+super(parent, name);+}++public void setVirtualName(String virtualName) {+this.virtualName = virtualName;+}++@Override+public String getName() {+if (virtualName != null) {+return virtualName;+} else {+return super.getName();+}+}++@Override+protected Class<CustomBuild> getBuildClass() {+return CustomBuild.class;+}++@Override+public TopLevelItemDescriptor getDescriptor() {+return (FreeStyleProject.DescriptorImpl) Jenkins.get().getDescriptorOrDie(getClass());+}++@TestExtension+public static class DescriptorImpl extends AbstractProjectDescriptor {++@Override+public FullNameChangingProject newInstance(ItemGroup parent, String name) {+return new FullNameChangingProject(parent, name);+}+}+}+}
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2020-2221
- WEBhttps://github.com/jenkinsci/jenkins/commit/f6e575381bdba85afaf27c529d7298091f226e49
- PACKAGEhttps://github.com/jenkinsci/jenkins
- WEBhttps://jenkins.io/security/advisory/2020-07-15/#SECURITY-1901
- WEBhttp://www.openwall.com/lists/oss-security/2020/07/15/5