Skip to content

Commit 18b3121

Browse files
Wadeckdaniel-beck
authored andcommitted
[SECURITY-440]
1 parent 822ece7 commit 18b3121

File tree

8 files changed

+150
-104
lines changed

8 files changed

+150
-104
lines changed

Diff for: pom.xml

+9-3
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@
6666
</scm>
6767

6868
<properties>
69-
<jenkins.version>1.609</jenkins.version>
70-
<java.level>6</java.level>
69+
<jenkins.version>1.625</jenkins.version>
70+
<java.level>7</java.level>
7171
</properties>
7272

7373
<repositories>
@@ -95,10 +95,16 @@
9595
<dependency>
9696
<groupId>org.jenkins-ci.plugins</groupId>
9797
<artifactId>credentials</artifactId>
98-
<version>2.1.0</version>
98+
<version>2.1.17</version>
9999
</dependency>
100100
<!-- jenkins dependencies -->
101101
<!-- test dependencies -->
102+
<dependency>
103+
<groupId>org.jenkins-ci.plugins</groupId>
104+
<artifactId>cloudbees-folder</artifactId>
105+
<version>6.4</version>
106+
<scope>test</scope>
107+
</dependency>
102108
</dependencies>
103109

104110
</project>

Diff for: src/main/java/com/cloudbees/jenkins/plugins/sshcredentials/impl/BasicSSHUserPrivateKey.java

+34-74
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,31 @@
2626
import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
2727
import com.cloudbees.plugins.credentials.CredentialsProvider;
2828
import com.cloudbees.plugins.credentials.CredentialsScope;
29-
import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker;
3029
import edu.umd.cs.findbugs.annotations.CheckForNull;
3130
import edu.umd.cs.findbugs.annotations.NonNull;
3231
import hudson.DescriptorExtensionList;
3332
import hudson.Extension;
3433
import hudson.model.AbstractDescribableImpl;
3534
import hudson.model.Descriptor;
35+
import hudson.model.Items;
3636
import hudson.remoting.Channel;
3737
import hudson.util.Secret;
3838
import java.io.File;
3939
import java.io.IOException;
4040
import java.io.ObjectStreamException;
4141
import java.io.Serializable;
4242
import java.io.StringReader;
43+
import java.lang.reflect.InvocationTargetException;
44+
import java.lang.reflect.Method;
4345
import java.util.ArrayList;
4446
import java.util.Arrays;
4547
import java.util.Collections;
4648
import java.util.List;
4749
import java.util.concurrent.TimeUnit;
4850
import java.util.logging.Level;
4951
import java.util.logging.Logger;
52+
53+
import hudson.util.XStream2;
5054
import jenkins.model.Jenkins;
5155
import net.jcip.annotations.GuardedBy;
5256
import org.apache.commons.io.FileUtils;
@@ -148,16 +152,6 @@ private synchronized Object readResolve() throws ObjectStreamException {
148152
return this;
149153
}
150154

151-
private Object writeReplace() {
152-
if (/* XStream */Channel.current() == null) {
153-
return this;
154-
}
155-
if (privateKeySource == null || privateKeySource.isSnapshotSource()) {
156-
return this;
157-
}
158-
return CredentialsProvider.snapshot(this);
159-
}
160-
161155
/**
162156
* {@inheritDoc}
163157
*/
@@ -290,7 +284,9 @@ public long getPrivateKeysLastModified() {
290284
*
291285
* @return {@code true} if and only if the source is self contained.
292286
* @since 1.7
287+
* @deprecated no more used since FileOnMaster- and Users- PrivateKeySource are deprecated too
293288
*/
289+
@Deprecated
294290
public boolean isSnapshotSource() {
295291
return false;
296292
}
@@ -371,7 +367,9 @@ public String getDisplayName() {
371367

372368
/**
373369
* Let the user reference a file on the disk.
370+
* @deprecated This approach has security vulnerability and should be migrated to {@link DirectEntryPrivateKeySource}
374371
*/
372+
@Deprecated
375373
public static class FileOnMasterPrivateKeySource extends PrivateKeySource {
376374

377375
/**
@@ -394,8 +392,6 @@ public static class FileOnMasterPrivateKeySource extends PrivateKeySource {
394392
*/
395393
private transient volatile long nextCheckLastModified;
396394

397-
398-
@DataBoundConstructor
399395
public FileOnMasterPrivateKeySource(String privateKeyFile) {
400396
this.privateKeyFile = privateKeyFile;
401397
}
@@ -436,7 +432,12 @@ private Object readResolve() {
436432
// this is a borked upgrade, not actually the file name but is actually the key contents
437433
return new DirectEntryPrivateKeySource(privateKeyFile);
438434
}
439-
return this;
435+
436+
Jenkins.getActiveInstance().checkPermission(Jenkins.RUN_SCRIPTS);
437+
438+
LOGGER.log(Level.INFO, "SECURITY-440: Migrating FileOnMasterPrivateKeySource to DirectEntryPrivateKeySource");
439+
// read the content of the file and then migrate to Direct
440+
return new DirectEntryPrivateKeySource(getPrivateKeys());
440441
}
441442

442443
@Override
@@ -453,26 +454,13 @@ public long getPrivateKeysLastModified() {
453454
}
454455
return lastModified;
455456
}
456-
457-
/**
458-
* {@inheritDoc}
459-
*/
460-
@Extension
461-
public static class DescriptorImpl extends PrivateKeySourceDescriptor {
462-
463-
/**
464-
* {@inheritDoc}
465-
*/
466-
@Override
467-
public String getDisplayName() {
468-
return Messages.BasicSSHUserPrivateKey_FileOnMasterPrivateKeySourceDisplayName();
469-
}
470-
}
471457
}
472458

473459
/**
474460
* Let the user
461+
* @deprecated This approach has security vulnerability and should be migrated to {@link DirectEntryPrivateKeySource}
475462
*/
463+
@Deprecated
476464
public static class UsersPrivateKeySource extends PrivateKeySource {
477465

478466
/**
@@ -490,10 +478,6 @@ public static class UsersPrivateKeySource extends PrivateKeySource {
490478
*/
491479
private transient volatile long nextCheckLastModified;
492480

493-
@DataBoundConstructor
494-
public UsersPrivateKeySource() {
495-
}
496-
497481
private List<File> files() {
498482
List<File> files = new ArrayList<File>();
499483
File sshHome = new File(new File(System.getProperty("user.home")), ".ssh");
@@ -535,51 +519,27 @@ public long getPrivateKeysLastModified() {
535519
return lastModified;
536520
}
537521

538-
/**
539-
* {@inheritDoc}
540-
*/
541-
@Extension
542-
public static class DescriptorImpl extends PrivateKeySourceDescriptor {
522+
private Object readResolve() {
523+
Jenkins.getActiveInstance().checkPermission(Jenkins.RUN_SCRIPTS);
543524

544-
/**
545-
* {@inheritDoc}
546-
*/
547-
@Override
548-
public String getDisplayName() {
549-
return Messages.BasicSSHUserPrivateKey_UsersPrivateKeySourceDisplayName();
550-
}
525+
LOGGER.log(Level.INFO, "SECURITY-440: Migrating UsersPrivateKeySource to DirectEntryPrivateKeySource");
526+
// read the content of the file and then migrate to Direct
527+
return new DirectEntryPrivateKeySource(getPrivateKeys());
551528
}
552529
}
553530

554-
/**
555-
* @since 1.7
556-
*/
557-
@Extension
558-
public static class CredentialsSnapshotTakerImpl extends CredentialsSnapshotTaker<SSHUserPrivateKey> {
559-
560-
/**
561-
* {@inheritDoc}
562-
*/
563-
@Override
564-
public Class<SSHUserPrivateKey> type() {
565-
return SSHUserPrivateKey.class;
566-
}
567-
568-
/**
569-
* {@inheritDoc}
570-
*/
571-
@Override
572-
public SSHUserPrivateKey snapshot(SSHUserPrivateKey credentials) {
573-
if (credentials instanceof BasicSSHUserPrivateKey) {
574-
final PrivateKeySource keySource = ((BasicSSHUserPrivateKey) credentials).getPrivateKeySource();
575-
if (keySource.isSnapshotSource()) {
576-
return credentials;
577-
}
578-
}
579-
final Secret passphrase = credentials.getPassphrase();
580-
return new BasicSSHUserPrivateKey(credentials.getScope(), credentials.getId(), credentials.getUsername(),
581-
new DirectEntryPrivateKeySource(credentials.getPrivateKeys()),
582-
passphrase == null ? null : passphrase.getEncryptedValue(), credentials.getDescription());
531+
static {
532+
try {
533+
// the critical field allow the permission check to make the XML read to fail completely in case of violation
534+
// TODO: Remove reflection once baseline is updated past 2.85.
535+
Method m = XStream2.class.getMethod("addCriticalField", Class.class, String.class);
536+
m.invoke(Items.XSTREAM2, BasicSSHUserPrivateKey.class, "privateKeySource");
537+
} catch (IllegalAccessException e) {
538+
throw new ExceptionInInitializerError(e);
539+
} catch (InvocationTargetException e) {
540+
throw new ExceptionInInitializerError(e);
541+
} catch (NoSuchMethodException e) {
542+
throw new ExceptionInInitializerError(e);
583543
}
584544
}
585545
}

Diff for: src/main/resources/com/cloudbees/jenkins/plugins/sshcredentials/impl/Messages.properties

+1-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,4 @@
2222
# THE SOFTWARE.
2323
#
2424
BasicSSHUserPrivateKey.DirectEntryPrivateKeySourceDisplayName=Enter directly
25-
BasicSSHUserPrivateKey.FileOnMasterPrivateKeySourceDisplayName=From a file on Jenkins master
26-
BasicSSHUserPrivateKey.UsersPrivateKeySourceDisplayName=From the Jenkins master ~/.ssh
27-
BasicSSHUserPrivateKey.DisplayName=SSH Username with private key
25+
BasicSSHUserPrivateKey.DisplayName=SSH Username with private key

Diff for: src/main/resources/com/cloudbees/jenkins/plugins/sshcredentials/impl/Messages_de.properties

+1-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,4 @@
2222
# THE SOFTWARE.
2323
#
2424
BasicSSHUserPrivateKey.DirectEntryPrivateKeySourceDisplayName=Direkt eingeben
25-
BasicSSHUserPrivateKey.FileOnMasterPrivateKeySourceDisplayName=Aus einer Datei auf dem Jenkins Master
26-
BasicSSHUserPrivateKey.UsersPrivateKeySourceDisplayName=Aus ~/.ssh des Jenkins Masters
27-
BasicSSHUserPrivateKey.DisplayName=SSH Benutzername und privater Schl\u00fcssel
25+
BasicSSHUserPrivateKey.DisplayName=SSH Benutzername und privater Schl\u00fcssel

Diff for: src/main/resources/com/cloudbees/jenkins/plugins/sshcredentials/impl/Messages_ja.properties

+1-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,4 @@
2222
# THE SOFTWARE.
2323
#
2424
BasicSSHUserPrivateKey.DirectEntryPrivateKeySourceDisplayName=\u76f4\u63a5\u5165\u529b
25-
BasicSSHUserPrivateKey.FileOnMasterPrivateKeySourceDisplayName=Jenkins\u30de\u30b9\u30bf\u30fc\u4e0a\u306e\u30d5\u30a1\u30a4\u30eb\u304b\u3089
26-
BasicSSHUserPrivateKey.UsersPrivateKeySourceDisplayName=Jenkins\u30de\u30b9\u30bf\u30fc\u4e0a\u306e~/.ssh\u304b\u3089
27-
BasicSSHUserPrivateKey.DisplayName=SSH \u30e6\u30fc\u30b6\u30fc\u540d\u3068\u79d8\u5bc6\u9375
25+
BasicSSHUserPrivateKey.DisplayName=SSH \u30e6\u30fc\u30b6\u30fc\u540d\u3068\u79d8\u5bc6\u9375

Diff for: src/test/java/com/cloudbees/jenkins/plugins/sshcredentials/impl/BasicSSHUserPrivateKeyTest.java

-18
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,6 @@ public class BasicSSHUserPrivateKeyTest {
5151

5252
@Rule public JenkinsRule r = new JenkinsRule();
5353

54-
@Test public void masterKeysOnSlave() throws Exception {
55-
FilePath keyfile = r.jenkins.getRootPath().child("key");
56-
keyfile.write("stuff", null);
57-
SSHUserPrivateKey key = new BasicSSHUserPrivateKey(CredentialsScope.SYSTEM, "mycreds", "git", new BasicSSHUserPrivateKey.FileOnMasterPrivateKeySource(keyfile.getRemote()), null, null);
58-
assertEquals("[stuff]", key.getPrivateKeys().toString());
59-
// TODO would be more interesting to use a Docker fixture to demonstrate that the file load is happening only from the master side
60-
assertEquals("[stuff]", r.createOnlineSlave().getChannel().call(new LoadPrivateKeys(key)));
61-
}
62-
private static class LoadPrivateKeys extends MasterToSlaveCallable<String,Exception> {
63-
private final SSHUserPrivateKey key;
64-
LoadPrivateKeys(SSHUserPrivateKey key) {
65-
this.key = key;
66-
}
67-
@Override public String call() throws Exception {
68-
return key.getPrivateKeys().toString();
69-
}
70-
}
71-
7254
@LocalData
7355
@Test
7456
public void readOldCredentials() throws Exception {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.cloudbees.jenkins.plugins.sshcredentials.impl;
2+
3+
import com.cloudbees.hudson.plugins.folder.Folder;
4+
import hudson.FilePath;
5+
import hudson.cli.CLICommandInvoker;
6+
import hudson.cli.UpdateJobCommand;
7+
import hudson.model.Job;
8+
import jenkins.model.Jenkins;
9+
import org.junit.Rule;
10+
import org.junit.Test;
11+
import org.jvnet.hudson.test.Issue;
12+
import org.jvnet.hudson.test.JenkinsRule;
13+
import org.jvnet.hudson.test.recipes.LocalData;
14+
15+
import static hudson.cli.CLICommandInvoker.Matcher.failedWith;
16+
import static hudson.cli.CLICommandInvoker.Matcher.succeeded;
17+
import static org.hamcrest.MatcherAssert.assertThat;
18+
import static org.hamcrest.Matchers.containsString;
19+
import static org.hamcrest.Matchers.not;
20+
21+
//TODO merge it into BasicSSHUserPrivateKeyTest after security patch
22+
public class BasicSSHUserPrivateKeyTest_SEC440 {
23+
24+
@Rule
25+
public JenkinsRule r = new JenkinsRule();
26+
27+
{r.timeout = 0;}
28+
29+
@Test
30+
@Issue("SECURITY-440")
31+
@LocalData("updateJob")
32+
public void userWithoutRunScripts_cannotMigrateDangerousPrivateKeySource() throws Exception {
33+
Folder folder = r.jenkins.createProject(Folder.class, "folder1");
34+
35+
FilePath updateFolder = r.jenkins.getRootPath().child("update_folder.xml");
36+
37+
{ // as user with just configure, you cannot migrate
38+
CLICommandInvoker.Result result = new CLICommandInvoker(r, new UpdateJobCommand())
39+
.authorizedTo(Jenkins.READ, Job.READ, Job.CONFIGURE)
40+
.withStdin(updateFolder.read())
41+
.invokeWithArgs("folder1");
42+
43+
assertThat(result.stderr(), containsString("user is missing the Overall/RunScripts permission"));
44+
assertThat(result, failedWith(-1));
45+
46+
// config file not touched
47+
String configFileContent = folder.getConfigFile().asString();
48+
assertThat(configFileContent, not(containsString("FileOnMasterPrivateKeySource")));
49+
assertThat(configFileContent, not(containsString("BasicSSHUserPrivateKey")));
50+
}
51+
{ // but as admin with RUN_SCRIPTS, you can
52+
CLICommandInvoker.Result result = new CLICommandInvoker(r, new UpdateJobCommand())
53+
.authorizedTo(Jenkins.ADMINISTER)
54+
.withStdin(updateFolder.read())
55+
.invokeWithArgs("folder1");
56+
57+
assertThat(result, succeeded());
58+
String configFileContent = folder.getConfigFile().asString();
59+
assertThat(configFileContent, containsString("BasicSSHUserPrivateKey"));
60+
assertThat(configFileContent, not(containsString("FileOnMasterPrivateKeySource")));
61+
}
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<com.cloudbees.hudson.plugins.folder.Folder plugin="[email protected]">
3+
<actions/>
4+
<description></description>
5+
<properties>
6+
<com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider_-FolderCredentialsProperty>
7+
<domainCredentialsMap class="hudson.util.CopyOnWriteMap$Hash">
8+
<entry>
9+
<com.cloudbees.plugins.credentials.domains.Domain plugin="[email protected]">
10+
<specifications/>
11+
</com.cloudbees.plugins.credentials.domains.Domain>
12+
<java.util.concurrent.CopyOnWriteArrayList>
13+
<com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey plugin="[email protected]">
14+
<id>From SSH</id>
15+
<username>from_ssh</username>
16+
<privateKeySource class="com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$UsersPrivateKeySource"/>
17+
</com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey>
18+
</java.util.concurrent.CopyOnWriteArrayList>
19+
</entry>
20+
</domainCredentialsMap>
21+
</com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider_-FolderCredentialsProperty>
22+
</properties>
23+
<folderViews class="com.cloudbees.hudson.plugins.folder.views.DefaultFolderViewHolder">
24+
<views>
25+
<hudson.model.AllView>
26+
<owner class="com.cloudbees.hudson.plugins.folder.Folder" reference="../../../.."/>
27+
<name>All</name>
28+
<filterExecutors>false</filterExecutors>
29+
<filterQueue>false</filterQueue>
30+
<properties class="hudson.model.View$PropertyList"/>
31+
</hudson.model.AllView>
32+
</views>
33+
<tabBar class="hudson.views.DefaultViewsTabBar"/>
34+
</folderViews>
35+
<healthMetrics>
36+
<com.cloudbees.hudson.plugins.folder.health.WorstChildHealthMetric>
37+
<nonRecursive>false</nonRecursive>
38+
</com.cloudbees.hudson.plugins.folder.health.WorstChildHealthMetric>
39+
</healthMetrics>
40+
<icon class="com.cloudbees.hudson.plugins.folder.icons.StockFolderIcon"/>
41+
</com.cloudbees.hudson.plugins.folder.Folder>

0 commit comments

Comments
 (0)