CVE-2017-2603 (XML, published 2018-05-15T21:29Z)
Description: value: Jenkins before versions 2.44, 2.32.2 is vulnerable to a user data leak in disconnected agents' config.xml API. This could leak sensitive data such as API tokens (SECURITY-362).
Severity: LOW (CVSS v3 base score: 3.5)
Weakness: CWE-200 \u2014 Exposure of Sensitive Information to an Unauthorized Actor
The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
Repository: https://github.com/jenkinsci/jenkins
Files changed:
core/src/main/java/hudson/slaves/OfflineCause.java
test/src/test/java/hudson/model/ComputerTest.java
test/src/test/resources/hudson/model/ComputerTest/removeUserDetailsFromOfflineCause/config.xml
test/src/test/resources/hudson/model/ComputerTest/removeUserDetailsFromOfflineCause/nodes/deserialized/config.xml
test/src/test/resources/hudson/model/ComputerTest/removeUserDetailsFromOfflineCause/users/username/config.xml
Commit message: Merge pull request #80 from jenkinsci-cert/SECURITY-362

[SECURITY-362] Do not persist User in OfflineCause.UserCause

Vulnerable code:
import jenkins.model.Jenkins;
        private final User user;

        public UserCause(User user, String message) {
            super(hudson.slaves.Messages._SlaveComputer_DisconnectedBy(
                    user!=null ? user.getId() : Jenkins.ANONYMOUS.getName(),
            ));
            this.user = user;
            return user;

Fixed code:
import javax.annotation.CheckForNull;
import java.io.ObjectStreamException;
import java.util.Collections;
     *
        @Deprecated
        private transient User user;
        // null when unknown
        private /*final*/ @CheckForNull String userId;

        public UserCause(@CheckForNull User user, @CheckForNull String message) {
            this(
                    user != null ? user.getId() : null,
            );
        }

        private UserCause(String userId, String message) {
            super(hudson.slaves.Messages._SlaveComputer_DisconnectedBy(userId, message));
            this.userId = userId;
            return userId == null
                    ? User.getUnknown()
                    : User.getById(userId, true)
            ;
        }

        // Storing the User in a filed was a mistake, switch to userId
        @SuppressWarnings("deprecation")
        private Object readResolve() throws ObjectStreamException {
            if (user != null) {
                String id = user.getId();
                if (id != null) {
                    userId = id;
                } else {
                    // The user field is not properly deserialized so id may be missing. Look the user up by fullname
                    User user = User.get(this.user.getFullName(), true, Collections.emptyMap());
                    userId = user.getId();
                }
                this.user = null;
            }
            return this;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;

import com.gargoylesoftware.htmlunit.xml.XmlPage;
import hudson.slaves.OfflineCause;
import org.jvnet.hudson.test.recipes.LocalData;

    @Test
    public void doNotShowUserDetailsInOfflineCause() throws Exception {
        DumbSlave slave = j.createOnlineSlave();
        final Computer computer = slave.toComputer();
        computer.setTemporarilyOffline(true, new OfflineCause.UserCause(User.get("username"), "msg"));
        verifyOfflineCause(computer);
    }

    @Test @LocalData
    public void removeUserDetailsFromOfflineCause() throws Exception {
        Computer computer = j.jenkins.getComputer("deserialized");
        verifyOfflineCause(computer);
    }

    private void verifyOfflineCause(Computer computer) throws Exception {
        XmlPage page = j.createWebClient().goToXml("computer/" + computer.getName() + "/config.xml");
        String content = page.getWebResponse().getContentAsString("UTF-8");
        assertThat(content, containsString("temporaryOfflineCause"));
        assertThat(content, containsString("<userId>username</userId>"));
        assertThat(content, not(containsString("ApiTokenProperty")));
        assertThat(content, not(containsString("apiToken")));
    }
<?xml version='1.0' encoding='UTF-8'?>
<hudson>
  <disabledAdministrativeMonitors/>
  <version>1.0</version>
  <numExecutors>2</numExecutors>
  <mode>NORMAL</mode>
  <useSecurity>true</useSecurity>
  <authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
  <securityRealm class="hudson.security.SecurityRealm$None"/>
  <disableRememberMe>false</disableRememberMe>
  <projectNamingStrategy class="jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy"/>
  <workspaceDir>${JENKINS_HOME}/workspace/${ITEM_FULLNAME}</workspaceDir>
  <buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
  <jdks/>
  <viewsTabBar class="hudson.views.DefaultViewsTabBar"/>
  <myViewsTabBar class="hudson.views.DefaultMyViewsTabBar"/>
  <clouds/>
  <scmCheckoutRetryCount>0</scmCheckoutRetryCount>
  <views>
    <hudson.model.AllView>
      <owner class="hudson" reference="../../.."/>
      <name>All</name>
      <filterExecutors>false</filterExecutors>
      <filterQueue>false</filterQueue>
      <properties class="hudson.model.View$PropertyList"/>
    </hudson.model.AllView>
  </views>
  <primaryView>All</primaryView>
  <slaveAgentPort>0</slaveAgentPort>
  <label></label>
  <nodeProperties/>
  <globalNodeProperties/>
  <noUsageStatistics>true</noUsageStatistics>
</hudson>
<?xml version='1.0' encoding='UTF-8'?>
<slave>
    <temporaryOfflineCause class="hudson.slaves.OfflineCause$UserCause">
        <timestamp>1479196265920</timestamp>
        <description>
            <holder>
                <owner>hudson.slaves.Messages</owner>
            </holder>
            <key>SlaveComputer.DisconnectedBy</key>
            <args>
                <string>username</string>
                <string> : msg</string>
            </args>
        </description>
        <user>
            <fullName>username</fullName>
            <properties>
                <jenkins.security.ApiTokenProperty>
                    <apiToken>wPfVKd4HGJzRoEpazbTu35nXXfI34cguPjm+5JPO7pZDFLFgpFLviQsS3NdJndax</apiToken>
                </jenkins.security.ApiTokenProperty>
                <hudson.tasks.Mailer_-UserProperty/>
                <hudson.model.MyViewsProperty>
                    <views>
                        <hudson.model.AllView>
                            <owner class="hudson.model.MyViewsProperty" reference="../../.."/>
                            <name>All</name>
                            <filterExecutors>false</filterExecutors>
                            <filterQueue>false</filterQueue>
                            <properties class="hudson.model.View$PropertyList"/>
                        </hudson.model.AllView>
                    </views>
                </hudson.model.MyViewsProperty>
                <hudson.model.PaneStatusProperties>
                    <collapsed/>
                </hudson.model.PaneStatusProperties>
                <hudson.search.UserSearchProperty>
                    <insensitiveSearch>false</insensitiveSearch>
                </hudson.search.UserSearchProperty>
            </properties>
        </user>
    </temporaryOfflineCause>
    <name>deserialized</name>
    <description>dummy</description>
    <remoteFS>...</remoteFS>
    <numExecutors>1</numExecutors>
    <mode>NORMAL</mode>
    <retentionStrategy class="hudson.slaves.RetentionStrategy$2">
        <DESCRIPTOR>
            <outer-class reference="../.."/>
        </DESCRIPTOR>
    </retentionStrategy>
    <launcher/>
    <label></label>
    <nodeProperties/>
    <userId>SYSTEM</userId>
</slave>
<?xml version='1.0' encoding='UTF-8'?>
<user>
  <fullName>username</fullName>
  <id>username</id>
  <description></description>
  <properties>
    <jenkins.security.ApiTokenProperty>
      <apiToken>qykj8q6EqvMg9LPu+lCqLiXBZvEVdCTWoYJwmicXgH+yh1ZUm85iHe29grd+g3QG</apiToken>
    </jenkins.security.ApiTokenProperty>
    <com.cloudbees.plugins.credentials.UserCredentialsProvider_-UserCredentialsProperty plugin="credentials@1.18">
      <domainCredentialsMap class="hudson.util.CopyOnWriteMap$Hash"/>
    </com.cloudbees.plugins.credentials.UserCredentialsProvider_-UserCredentialsProperty>
    <hudson.tasks.Mailer_-UserProperty>
      <emailAddress></emailAddress>
    </hudson.tasks.Mailer_-UserProperty>
    <hudson.model.MyViewsProperty>
      <primaryViewName></primaryViewName>
      <views>
        <hudson.model.AllView>
          <owner class="hudson.model.MyViewsProperty" reference="../../.."/>
          <name>All</name>
          <filterExecutors>false</filterExecutors>
          <filterQueue>false</filterQueue>
          <properties class="hudson.model.View$PropertyList"/>
        </hudson.model.AllView>
      </views>
    </hudson.model.MyViewsProperty>
    <hudson.model.PaneStatusProperties>
      <collapsed/>
    </hudson.model.PaneStatusProperties>
    <org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl>
      <authorizedKeys></authorizedKeys>
    </org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl>
    <hudson.search.UserSearchProperty>
      <insensitiveSearch>false</insensitiveSearch>
    </hudson.search.UserSearchProperty>
  </properties>
</user>