High severity7.5CISA KEVNVD Advisory· Published Nov 25, 2015· Updated Apr 22, 2026
CVE-2015-5317
CVE-2015-5317
Description
The Fingerprints pages in Jenkins before 1.638 and LTS before 1.625.2 might allow remote attackers to obtain sensitive job and build name information via a direct request.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.main:jenkins-coreMaven | < 1.625.2 | 1.625.2 |
org.jenkins-ci.main:jenkins-coreMaven | >= 1.626, < 1.638 | 1.638 |
Affected products
4Patches
10594c4cbccd2[SECURITY-153] Hide references to inaccessible jobs in fingerprints.
7 files changed · +841 −27
core/src/main/java/hudson/model/Fingerprint.java+109 −10 modified@@ -39,7 +39,9 @@ import hudson.Extension; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SaveableListener; +import hudson.remoting.Callable; import hudson.security.ACL; +import hudson.security.Permission; import hudson.util.AtomicFileWriter; import hudson.util.HexBinaryConverter; import hudson.util.Iterators; @@ -71,6 +73,9 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import org.acegisecurity.AccessDeniedException; +import org.acegisecurity.Authentication; import org.xmlpull.v1.XmlPullParserException; /** @@ -103,15 +108,37 @@ public BuildPtr(Run run) { /** * Gets {@link Job#getFullName() the full name of the job}. - * <p> - * Such job could be since then removed, - * so there might not be a corresponding - * {@link Job}. + * Such job could be since then removed, so there might not be a corresponding {@link Job}. + * + * @return A name of the job */ @Exported + @Nonnull public String getName() { return name; } + + /** + * Checks if the current user has permission to see this pointer. + * @return {@code true} if the job exists and user has {@link Item#READ} permissions + * or if the current user has {@link Jenkins#ADMINISTER} permissions. + * If the job exists, but the current user has no permission to discover it, + * {@code false} will be returned. + * If the job has been deleted and the user has no {@link Jenkins#ADMINISTER} permissions, + * it also returns {@code false} in order to avoid the job existence fact exposure. + */ + private boolean hasPermissionToDiscoverBuild() { + // We expose the data to Jenkins administrators in order to + // let them manage the data for deleted jobs (also works for SYSTEM) + final Jenkins instance = Jenkins.getInstance(); + if (instance != null && instance.hasPermission(Jenkins.ADMINISTER)) { + return true; + } + + return canDiscoverItem(name); + } + + void setName(String newName) { name = newName; @@ -129,10 +156,11 @@ public Job<?,?> getJob() { /** * Gets the project build number. * <p> - * Such {@link Run} could be since then - * discarded. + * Such {@link Run} could be since then discarded. + * @return A build number */ @Exported + @Nonnull public int getNumber() { return number; } @@ -806,11 +834,15 @@ public Fingerprint(Run build, String fileName, byte[] md5sum) throws IOException * this file. * * @return null - * if the file is apparently created outside Hudson. + * if the file is apparently created outside Hudson or if the current + * user has no permission to discover the job. */ @Exported public BuildPtr getOriginal() { - return original; + if (original != null && original.hasPermissionToDiscoverBuild()) { + return original; + } + return null; } public String getDisplayName() { @@ -899,8 +931,17 @@ public RangeItem(String name, RangeSet ranges) { @Exported(name="usage") public List<RangeItem> _getUsages() { List<RangeItem> r = new ArrayList<RangeItem>(); - for (Entry<String, RangeSet> e : usages.entrySet()) - r.add(new RangeItem(e.getKey(),e.getValue())); + final Jenkins instance = Jenkins.getInstance(); + if (instance == null) { + return r; + } + + for (Entry<String, RangeSet> e : usages.entrySet()) { + final String itemName = e.getKey(); + if (instance.hasPermission(Jenkins.ADMINISTER) || canDiscoverItem(itemName)) { + r.add(new RangeItem(itemName, e.getValue())); + } + } return r; } @@ -1292,6 +1333,64 @@ private static String messageOfParseException(Throwable t) { @Override public String toString() { return "Fingerprint[original=" + original + ",hash=" + getHashString() + ",fileName=" + fileName + ",timestamp=" + DATE_CONVERTER.toString(timestamp) + ",usages=" + new TreeMap<String,RangeSet>(usages) + ",facets=" + facets + "]"; } + + /** + * Checks if the current user can Discover the item. + * If yes, it may be displayed as a text in Fingerprint UIs. + * @param fullName Full name of the job + * @return {@code true} if the user can discover the item + */ + private static boolean canDiscoverItem(@Nonnull final String fullName) { + final Jenkins jenkins = Jenkins.getInstance(); + if (jenkins == null) { + return false; + } + + // Fast check to avoid security context switches + Item item = null; + try { + item = jenkins.getItemByFullName(fullName); + } catch (AccessDeniedException ex) { + // ignore, we will fall-back later + } + if (item != null) { + return true; + } + + // Probably it failed due to the missing Item.DISCOVER + // We try to retrieve the job using SYSTEM user and to check permissions manually. + final Authentication userAuth = Jenkins.getAuthentication(); + final boolean[] res = new boolean[] {false}; + ACL.impersonate(ACL.SYSTEM, new Runnable() { + @Override + public void run() { + final Item itemBySystemUser = jenkins.getItemByFullName(fullName); + if (itemBySystemUser == null) { + return; + } + + // To get the item existence fact, a user needs Item.DISCOVER for the item + // and Item.READ for all container folders. + boolean canDiscoverTheItem = itemBySystemUser.getACL().hasPermission(userAuth, Item.DISCOVER); + if (canDiscoverTheItem) { + ItemGroup<?> current = itemBySystemUser.getParent(); + do { + if (current instanceof Item) { + final Item item = (Item) current; + current = item.getParent(); + if (!item.getACL().hasPermission(userAuth, Item.READ)) { + canDiscoverTheItem = false; + } + } else { + current = null; + } + } while (canDiscoverTheItem && current != null); + } + res[0] = canDiscoverTheItem; + } + }); + return res[0]; + } private static final XStream XSTREAM = new XStream2(); static {
core/src/main/resources/hudson/model/Fingerprint/index.jelly+12 −16 modified@@ -66,24 +66,20 @@ THE SOFTWARE. ${%This file has been used in the following places}: </p> <table class="fingerprint-summary"> + <j:set var="usages" value="${it.usages}"/> <j:forEach var="j" items="${it.jobs}"> <j:set var="job" value="${app.getItemByFullName(j)}" /> - <j:set var="range" value="${it.usages[j]}" /> - <tr> - <td class="fingerprint-summary-header"> - <j:choose> - <j:when test="${job!=null}"> - <a href="${rootURL}/${job.url}" class="model-link inside">${j}</a> - </j:when> - <j:otherwise> - ${j} - </j:otherwise> - </j:choose> - </td> - <td> - <t:buildRangeLink job="${job}" range="${range}" /> - </td> - </tr> + <j:set var="range" value="${usages[j]}" /> + <j:if test="${job!=null}"> <!--Otherwise we don't display links at all--> + <tr> + <td class="fingerprint-summary-header"> + <a href="${rootURL}/${job.url}" class="model-link inside">${j}</a> + <td> + <t:buildRangeLink job="${job}" range="${range}" /> + </td> + </td> + </tr> + </j:if> </j:forEach> </table> </j:otherwise>
test/src/main/java/org/jvnet/hudson/test/CreateFileBuilder.java+99 −0 added@@ -0,0 +1,99 @@ +/* + * The MIT License + * + * Copyright (c) 2015 Oleg Nenashev. + * + * 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 org.jvnet.hudson.test; + +import hudson.AbortException; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.tasks.Builder; +import java.io.IOException; +import javax.annotation.Nonnull; +import net.sf.json.JSONObject; +import org.kohsuke.stapler.StaplerRequest; + +/** + * Creates a test builder, which creates a file in the workspace. + * @author Oleg Nenashev + * @since TODO + */ +public class CreateFileBuilder extends Builder { + + @Nonnull + private final String fileName; + + @Nonnull + private final String fileContent; + + public CreateFileBuilder(@Nonnull String fileName, @Nonnull String fileContent) { + this.fileName = fileName; + this.fileContent = fileContent; + } + + @Nonnull + public String getFileName() { + return fileName; + } + + @Nonnull + public String getFileContent() { + return fileContent; + } + + @Override + public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + listener.getLogger().println("Creating a file " + fileName); + + FilePath workspace = build.getWorkspace(); + if (workspace == null) { + throw new AbortException("Cannot get the workspace of the build"); + } + workspace.child(fileName).write(fileContent, "UTF-8"); + + return true; + } + + @Override + public Descriptor<Builder> getDescriptor() { + return new DescriptorImpl(); + } + + @Extension + public static final class DescriptorImpl extends Descriptor<Builder> { + + @Override + public Builder newInstance(StaplerRequest req, JSONObject data) { + throw new UnsupportedOperationException("This is a temporarytest class, " + + "which should not be configured from UI"); + } + + @Override + public String getDisplayName() { + return "Create a file"; + } + } +}
test/src/main/java/org/jvnet/hudson/test/MockFolder.java+1 −1 modified@@ -77,7 +77,7 @@ public class MockFolder extends AbstractItem implements DirectlyModifiableTopLev private String primaryView; private ViewsTabBar viewsTabBar; - private MockFolder(ItemGroup parent, String name) { + protected MockFolder(ItemGroup parent, String name) { super(parent, name); }
test/src/main/java/org/jvnet/hudson/test/SecuredMockFolder.java+125 −0 added@@ -0,0 +1,125 @@ +/* + * The MIT License + * + * Copyright (c) 2015 Oleg Nenashev. + * + * 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 org.jvnet.hudson.test; + +import hudson.Extension; +import hudson.model.Item; +import hudson.model.ItemGroup; +import hudson.model.TopLevelItem; +import hudson.model.TopLevelItemDescriptor; +import hudson.security.ACL; +import hudson.security.Permission; +import hudson.security.SidACL; +import hudson.security.SparseACL; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Nonnull; +import jenkins.model.Jenkins; +import org.acegisecurity.Authentication; +import org.acegisecurity.acls.sid.Sid; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Folder stub with a configurable permission control. + * The implementation secures the access to the item and to its children. + * @author Oleg Nenashev + * @since TODO + */ +@Restricted(NoExternalUse.class) // Unrestrict after integrating into Jenkins trunk +public class SecuredMockFolder extends MockFolder { + + private String grantedUser; + private Set<String> grantedPermissions; + + private SecuredMockFolder(ItemGroup parent, String name) { + super(parent, name); + } + + @Override + public TopLevelItem getItem(String name) { + final TopLevelItem item = super.getItem(name); + if (item != null && item.hasPermission(Item.READ)) { + return item; + } + return null; + } + + @Override + public boolean hasPermission(Permission p) { + if (super.hasPermission(p)) { + return true; + } + return hasPermissionInField(Jenkins.getAuthentication().getName(), p); + } + + private boolean hasPermissionInField(String sid, @Nonnull Permission p) { + if (sid.equals(grantedUser)) { + if (grantedPermissions != null && grantedPermissions.contains(p.getId())) { + return true; + } + } + return false; + } + + @Override + public ACL getACL() { + return new ACLWrapper(); + } + + public void setPermissions(String username, Permission... permissions) { + this.grantedUser = username; + if (grantedPermissions == null) { + grantedPermissions = new HashSet<String>(); + } else { + grantedPermissions.clear(); + } + for (Permission p : permissions) { + grantedPermissions.add(p.getId()); + } + } + + @Extension + public static class DescriptorImpl extends TopLevelItemDescriptor { + + @Override + public String getDisplayName() { + return "MockFolder with security control"; + } + + @Override + public TopLevelItem newInstance(ItemGroup parent, String name) { + return new SecuredMockFolder(parent, name); + } + } + + private class ACLWrapper extends SidACL { + + @Override + protected Boolean hasPermission(Sid p, Permission permission) { + //TODO: Handle globally defined permissions? + return SecuredMockFolder.this.hasPermissionInField(toString(p), permission); + } + } +}
test/src/main/java/org/jvnet/hudson/test/WorkspaceCopyFileBuilder.java+116 −0 added@@ -0,0 +1,116 @@ +/* + * The MIT License + * + * Copyright (c) 2015 Oleg Nenashev. + * + * 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 org.jvnet.hudson.test; + +import hudson.AbortException; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; +import hudson.model.BuildListener; +import hudson.model.Descriptor; +import hudson.tasks.Builder; +import java.io.IOException; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.kohsuke.stapler.StaplerRequest; + +/** + * Test Builder, which copies a file from a workspace of another job. + * Supports {@link AbstractProject}s only. + * @author Oleg Nenashev + */ +public class WorkspaceCopyFileBuilder extends Builder { + + private final String fileName; + private final String jobName; + private final int buildNumber; + + public WorkspaceCopyFileBuilder(String fileName, String jobName, int buildNumber) { + this.fileName = fileName; + this.jobName = jobName; + this.buildNumber = buildNumber; + } + + public int getBuildNumber() { + return buildNumber; + } + + public String getFileName() { + return fileName; + } + + public String getJobName() { + return jobName; + } + + @Override + public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + listener.getLogger().println("Copying a " + fileName + " from " + jobName + "#" + buildNumber); + + Jenkins inst = Jenkins.getInstance(); + AbstractProject<?,?> item = inst.getItemByFullName(jobName, AbstractProject.class); + if (item == null) { + throw new AbortException("Cannot find a source job: " + jobName); + } + + AbstractBuild<?,?> sourceBuild = item.getBuildByNumber(buildNumber); + if (sourceBuild == null) { + throw new AbortException("Cannot find a source build: " + jobName + "#" + buildNumber); + } + + FilePath sourceWorkspace = sourceBuild.getWorkspace(); + if (sourceWorkspace == null) { + throw new AbortException("Cannot get the source workspace from " + sourceBuild.getDisplayName()); + } + + FilePath workspace = build.getWorkspace(); + if (workspace == null) { + throw new IOException("Cannot get the workspace of the build"); + } + workspace.child(fileName).copyFrom(sourceWorkspace.child(fileName)); + + return true; + } + + @Override + public Descriptor<Builder> getDescriptor() { + return new DescriptorImpl(); + } + + @Extension + public static final class DescriptorImpl extends Descriptor<Builder> { + + @Override + public Builder newInstance(StaplerRequest req, JSONObject data) { + throw new UnsupportedOperationException(); + } + + @Override + public String getDisplayName() { + return "Copy a file from the workspace of another build"; + } + } +} \ No newline at end of file
test/src/test/java/hudson/model/FingerprintTest.java+379 −0 added@@ -0,0 +1,379 @@ +/* + * The MIT License + * + * Copyright (c) 2015 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.security.ACL; +import hudson.security.AuthorizationMatrixProperty; +import hudson.security.Permission; +import hudson.security.ProjectMatrixAuthorizationStrategy; +import hudson.tasks.ArtifactArchiver; +import hudson.tasks.Fingerprinter; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import jenkins.model.Jenkins; +import org.junit.Rule; +import org.junit.Test; +import org.junit.Before; +import org.jvnet.hudson.test.CreateFileBuilder; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.MockFolder; +import org.jvnet.hudson.test.SecuredMockFolder; +import org.jvnet.hudson.test.WorkspaceCopyFileBuilder; + +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + + +//TODO: Refactoring: Tests should be exchanged with FingerprinterTest somehow +/** + * Tests for the {@link Fingerprint} class. + * @author Oleg Nenashev + */ +public class FingerprintTest { + + @Rule + public JenkinsRule rule = new JenkinsRule(); + + @Before + public void setupRealm() { + rule.jenkins.setSecurityRealm(rule.createDummySecurityRealm()); + } + + @Test + public void shouldCreateFingerprintsForWorkspace() throws Exception { + FreeStyleProject project = rule.createFreeStyleProject(); + project.getBuildersList().add(new CreateFileBuilder("test.txt", "Hello, world!")); + project.getPublishersList().add(new Fingerprinter("test.txt", false)); + FreeStyleBuild build = rule.buildAndAssertSuccess(project); + + Fingerprint fp = getFingerprint(build, "test.txt"); + } + + @Test + public void shouldCreateFingerprintsForArtifacts() throws Exception { + FreeStyleProject project = rule.createFreeStyleProject(); + project.getBuildersList().add(new CreateFileBuilder("test.txt", "Hello, world!")); + ArtifactArchiver archiver = new ArtifactArchiver("test.txt"); + archiver.setFingerprint(true); + project.getPublishersList().add(archiver); + FreeStyleBuild build = rule.buildAndAssertSuccess(project); + + Fingerprint fp = getFingerprint(build, "test.txt"); + } + + @Test + public void shouldCreateUsageLinks() throws Exception { + // Project 1 + FreeStyleProject project = createAndRunProjectWithPublisher("fpProducer", "test.txt"); + final FreeStyleBuild build = project.getLastBuild(); + + // Project 2 + FreeStyleProject project2 = rule.createFreeStyleProject(); + project2.getBuildersList().add(new WorkspaceCopyFileBuilder("test.txt", project.getName(), build.getNumber())); + project2.getPublishersList().add(new Fingerprinter("test.txt")); + FreeStyleBuild build2 = rule.buildAndAssertSuccess(project2); + + Fingerprint fp = getFingerprint(build, "test.txt"); + + // Check references + Fingerprint.BuildPtr original = fp.getOriginal(); + assertEquals("Original reference contains a wrong job name", project.getName(), original.getName()); + assertEquals("Original reference contains a wrong build number", build.getNumber(), original.getNumber()); + + Hashtable<String, Fingerprint.RangeSet> usages = fp.getUsages(); + assertTrue("Usages do not have a reference to " + project, usages.containsKey(project.getName())); + assertTrue("Usages do not have a reference to " + project2, usages.containsKey(project2.getName())); + } + + @Test + @Issue("SECURITY-153") + public void shouldBeUnableToSeeJobsIfNoPermissions() throws Exception { + // Project 1 + final FreeStyleProject project1 = createAndRunProjectWithPublisher("fpProducer", "test.txt"); + final FreeStyleBuild build = project1.getLastBuild(); + + // Project 2 + final FreeStyleProject project2 = rule.createFreeStyleProject("project2"); + project2.getBuildersList().add(new WorkspaceCopyFileBuilder("test.txt", project1.getName(), build.getNumber())); + project2.getPublishersList().add(new Fingerprinter("test.txt")); + final FreeStyleBuild build2 = rule.buildAndAssertSuccess(project2); + + // Get fingerprint + final Fingerprint fp = getFingerprint(build, "test.txt"); + + // Init Users + User user1 = User.get("user1"); // can access project1 + User user2 = User.get("user2"); // can access project2 + User user3 = User.get("user3"); // cannot access anything + + // Project permissions + setupProjectMatrixAuthStrategy(Jenkins.READ); + setJobPermissionsOnce(project1, "user1", Item.READ, Item.DISCOVER); + setJobPermissionsOnce(project2, "user2", Item.READ, Item.DISCOVER); + + ACL.impersonate(user1.impersonate(), new Runnable() { + @Override + public void run() { + Fingerprint.BuildPtr original = fp.getOriginal(); + assertThat("user1 should be able to see the origin", fp.getOriginal(), notNullValue()); + assertEquals("user1 should be able to see the origin's project name", project1.getName(), original.getName()); + assertEquals("user1 should be able to see the origin's build number", build.getNumber(), original.getNumber()); + assertEquals("Only one usage should be visible to user1", 1, fp._getUsages().size()); + assertEquals("Only project1 should be visible to user1", project1.getFullName(), fp._getUsages().get(0).name); + } + }); + + ACL.impersonate(user2.impersonate(), new Runnable() { + @Override + public void run() { + assertThat("user2 should be unable to see the origin", fp.getOriginal(), nullValue()); + assertEquals("Only one usage should be visible to user2", 1, fp._getUsages().size()); + assertEquals("Only project2 should be visible to user2", project2.getFullName(), fp._getUsages().get(0).name); + } + }); + + ACL.impersonate(user3.impersonate(), new Runnable() { + @Override + public void run() { + Fingerprint.BuildPtr original = fp.getOriginal(); + assertThat("user3 should be unable to see the origin", fp.getOriginal(), nullValue()); + assertEquals("All usages should be invisible for user3", 0, fp._getUsages().size()); + } + }); + } + + @Test + public void shouldBeAbleToSeeOriginalWithDiscoverPermissionOnly() throws Exception { + // Setup the environment + final FreeStyleProject project = createAndRunProjectWithPublisher("project", "test.txt"); + final FreeStyleBuild build = project.getLastBuild(); + final Fingerprint fingerprint = getFingerprint(build, "test.txt"); + + // Init Users and security + User user1 = User.get("user1"); + setupProjectMatrixAuthStrategy(Jenkins.READ, Item.DISCOVER); + + ACL.impersonate(user1.impersonate(), new Runnable() { + @Override + public void run() { + Fingerprint.BuildPtr original = fingerprint.getOriginal(); + assertThat("user1 should able to see the origin", fingerprint.getOriginal(), notNullValue()); + assertEquals("user1 sees the wrong original name with Item.DISCOVER", project.getFullName(), original.getName()); + assertEquals("user1 sees the wrong original number with Item.DISCOVER", build.getNumber(), original.getNumber()); + assertEquals("Usage ref in fingerprint should be visible to user1", 1, fingerprint._getUsages().size()); + } + }); + } + + @Test + public void shouldBeAbleToSeeFingerprintsInReadableFolder() throws Exception { + final SecuredMockFolder folder = rule.jenkins.createProject(SecuredMockFolder.class, "folder"); + final FreeStyleProject project = createAndRunProjectWithPublisher(folder, "project", "test.txt"); + final FreeStyleBuild build = project.getLastBuild(); + final Fingerprint fingerprint = getFingerprint(build, "test.txt"); + + // Init Users and security + User user1 = User.get("user1"); + setupProjectMatrixAuthStrategy(false, Jenkins.READ, Item.DISCOVER); + setJobPermissionsOnce(project, "user1", Item.DISCOVER); // Prevents the fallback to the folder ACL + folder.setPermissions("user1", Item.READ); + + // Ensure we can read the original from user account + ACL.impersonate(user1.impersonate(), new Runnable() { + @Override + public void run() { + assertTrue("Test framework issue: User1 should be able to read the folder", folder.hasPermission(Item.READ)); + + Fingerprint.BuildPtr original = fingerprint.getOriginal(); + assertThat("user1 should able to see the origin", fingerprint.getOriginal(), notNullValue()); + assertEquals("user1 sees the wrong original name with Item.DISCOVER", project.getFullName(), original.getName()); + assertEquals("user1 sees the wrong original number with Item.DISCOVER", build.getNumber(), original.getNumber()); + assertEquals("user1 should be able to see the job", 1, fingerprint._getUsages().size()); + + assertThat("User should be unable do retrieve the job due to the missing read", original.getJob(), nullValue()); + } + }); + } + + @Test + public void shouldBeUnableToSeeFingerprintsInUnreadableFolder() throws Exception { + final SecuredMockFolder folder = rule.jenkins.createProject(SecuredMockFolder.class, "folder"); + final FreeStyleProject project = createAndRunProjectWithPublisher(folder, "project", "test.txt"); + final FreeStyleBuild build = project.getLastBuild(); + final Fingerprint fingerprint = getFingerprint(build, "test.txt"); + + // Init Users and security + User user1 = User.get("user1"); // can access project1 + setupProjectMatrixAuthStrategy(Jenkins.READ, Item.DISCOVER); + + // Ensure we can read the original from user account + ACL.impersonate(user1.impersonate(), new Runnable() { + @Override + public void run() { + assertFalse("Test framework issue: User1 should be unable to read the folder", folder.hasPermission(Item.READ)); + assertThat("user1 should be unable to see the origin", fingerprint.getOriginal(), nullValue()); + assertEquals("No jobs should be visible to user1", 0, fingerprint._getUsages().size()); + } + }); + } + + /** + * A common non-admin user should not be able to see references to a + * deleted job even if he used to have READ permissions before the deletion. + * @throws Exception Test error + */ + @Test + @Issue("SECURITY-153") + public void commonUserShouldBeUnableToSeeReferencesOfDeletedJobs() throws Exception { + // Setup the environment + FreeStyleProject project = createAndRunProjectWithPublisher("project", "test.txt"); + FreeStyleBuild build = project.getLastBuild(); + final Fingerprint fp = getFingerprint(build, "test.txt"); + + // Init Users and security + User user1 = User.get("user1"); + setupProjectMatrixAuthStrategy(Jenkins.READ, Item.READ, Item.DISCOVER); + project.delete(); + + ACL.impersonate(user1.impersonate(), new Runnable() { + @Override + public void run() { + assertThat("user1 should be unable to see the origin", fp.getOriginal(), nullValue()); + assertEquals("No jobs should be visible to user1", 0, fp._getUsages().size()); + } + }); + } + + @Test + public void adminShouldBeAbleToSeeReferencesOfDeletedJobs() throws Exception { + // Setup the environment + final FreeStyleProject project = createAndRunProjectWithPublisher("project", "test.txt"); + final FreeStyleBuild build = project.getLastBuild(); + final Fingerprint fingerprint = getFingerprint(build, "test.txt"); + + // Init Users and security + User user1 = User.get("user1"); + setupProjectMatrixAuthStrategy(Jenkins.ADMINISTER); + project.delete(); + + ACL.impersonate(user1.impersonate(), new Runnable() { + @Override + public void run() { + Fingerprint.BuildPtr original = fingerprint.getOriginal(); + assertThat("user1 should able to see the origin", fingerprint.getOriginal(), notNullValue()); + assertThat("Job has been deleted, so Job reference shoud return null", fingerprint.getOriginal().getJob(), nullValue()); + assertEquals("user1 sees the wrong original name with Item.DISCOVER", project.getFullName(), original.getName()); + assertEquals("user1 sees the wrong original number with Item.DISCOVER", build.getNumber(), original.getNumber()); + assertEquals("user1 should be able to see the job in usages", 1, fingerprint._getUsages().size()); + } + }); + } + + @Nonnull + private Fingerprint getFingerprint(@CheckForNull Run<?, ?> run, @Nonnull String filename) { + assertNotNull("Input run is null", run); + Fingerprinter.FingerprintAction action = run.getAction(Fingerprinter.FingerprintAction.class); + assertNotNull("Fingerprint action has not been created in " + run, action); + Map<String, Fingerprint> fingerprints = action.getFingerprints(); + final Fingerprint fp = fingerprints.get(filename); + assertNotNull("No reference to '" + filename + "' from the Fingerprint action", fp); + return fp; + } + + @Nonnull + private FreeStyleProject createAndRunProjectWithPublisher(String projectName, String fpFileName) + throws Exception { + return createAndRunProjectWithPublisher(null, projectName, fpFileName); + } + + @Nonnull + private FreeStyleProject createAndRunProjectWithPublisher(@CheckForNull MockFolder folder, + String projectName, String fpFileName) throws Exception { + final FreeStyleProject project; + if (folder == null) { + project = rule.createFreeStyleProject(projectName); + } else { + project = folder.createProject(FreeStyleProject.class, projectName); + } + project.getBuildersList().add(new CreateFileBuilder(fpFileName, "Hello, world!")); + ArtifactArchiver archiver = new ArtifactArchiver(fpFileName); + archiver.setFingerprint(true); + project.getPublishersList().add(archiver); + rule.buildAndAssertSuccess(project); + return project; + } + + private void setupProjectMatrixAuthStrategy(@Nonnull Permission ... permissions) { + setupProjectMatrixAuthStrategy(true, permissions); + } + + private void setupProjectMatrixAuthStrategy(boolean inheritFromFolders, @Nonnull Permission ... permissions) { + ProjectMatrixAuthorizationStrategy str = inheritFromFolders + ? new ProjectMatrixAuthorizationStrategy() + : new NoInheritanceProjectMatrixAuthorizationStrategy(); + for (Permission p : permissions) { + str.add(p, "anonymous"); + } + rule.jenkins.setAuthorizationStrategy(str); + } + //TODO: could be reworked to support multiple assignments + private void setJobPermissionsOnce(Job<?,?> job, String username, @Nonnull Permission ... s) + throws IOException { + assertThat("Cannot assign the property twice", job.getProperty(AuthorizationMatrixProperty.class), nullValue()); + + Map<Permission, Set<String>> permissions = new HashMap<Permission, Set<String>>(); + HashSet<String> userSpec = new HashSet<String>(Arrays.asList(username)); + + for (Permission p : s) { + permissions.put(p, userSpec); + } + AuthorizationMatrixProperty property = new AuthorizationMatrixProperty(permissions); + job.addProperty(property); + } + + /** + * Security strategy, which prevents the permission inheritance from upper folders. + */ + private static class NoInheritanceProjectMatrixAuthorizationStrategy extends ProjectMatrixAuthorizationStrategy { + + @Override + public ACL getACL(Job<?, ?> project) { + AuthorizationMatrixProperty amp = project.getProperty(AuthorizationMatrixProperty.class); + if (amp != null) { + return amp.getACL().newInheritingACL(getRootACL()); + } else { + return getRootACL(); + } + } + } +}
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
7- rhn.redhat.com/errata/RHSA-2016-0489.htmlnvdThird Party AdvisoryWEB
- access.redhat.com/errata/RHSA-2016:0070nvdThird Party AdvisoryWEB
- github.com/advisories/GHSA-8pqx-3rxx-f5pmghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2015-5317ghsaADVISORY
- wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2015-11-11nvdVendor AdvisoryWEB
- github.com/jenkinsci/jenkins/commit/0594c4cbccd24d4883fc0150e8fc511c9da63eb4ghsaWEB
- www.cisa.gov/known-exploited-vulnerabilities-catalognvdUS Government ResourceWEB
News mentions
0No linked articles in our index yet.