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

PackageAffected versionsPatched versions
org.jenkins-ci.main:jenkins-coreMaven
< 1.625.21.625.2
org.jenkins-ci.main:jenkins-coreMaven
>= 1.626, < 1.6381.638

Affected products

4
  • Jenkins/Jenkins2 versions
    cpe:2.3:a:jenkins:jenkins:*:*:*:*:-:*:*:*+ 1 more
    • cpe:2.3:a:jenkins:jenkins:*:*:*:*:-:*:*:*range: <=1.637
    • cpe:2.3:a:jenkins:jenkins:*:*:*:*:lts:*:*:*range: <=1.625.1
  • Red Hat/Openshift2 versions
    cpe:2.3:a:redhat:openshift:2.0:*:*:*:*:*:*:*+ 1 more
    • cpe:2.3:a:redhat:openshift:2.0:*:*:*:*:*:*:*
    • cpe:2.3:a:redhat:openshift:*:*:*:*:enterprise:*:*:*range: <=3.1

Patches

1
0594c4cbccd2

[SECURITY-153] Hide references to inaccessible jobs in fingerprints.

https://github.com/jenkinsci/jenkinsOleg NenashevOct 6, 2015via ghsa
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

News mentions

0

No linked articles in our index yet.