Low severityNVD Advisory· Published Feb 12, 2020· Updated Aug 4, 2024
CVE-2020-2126
CVE-2020-2126
Description
Jenkins DigitalOcean Plugin 1.1 and earlier stores a token unencrypted in the global config.xml file on the Jenkins master where it can be viewed by users with access to the master file system.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
com.dubture.jenkins:digitalocean-pluginMaven | < 1.2.0 | 1.2.0 |
Affected products
1- Range: unspecified
Patches
1ca1112c5c52aSwitch ssh keys and auth tokens to using credentials system (Fixes SECURITY-1559) (#50)
8 files changed · +363 −95
.editorconfig+25 −0 added@@ -0,0 +1,25 @@ +[*] +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +max_line_length = 80 +trim_trailing_whitespace = true + +[*.yml] +indent_size = 2 +max_line_length = 180 + +[*.md] +indent_size = 2 +max_line_length = 180 + +[*.json] +indent_size = 2 +max_line_length = 18000 + +[*.js] +indent_size = 2 + +[*.jsx] +indent_size = 2
pom.xml+59 −44 modified@@ -1,19 +1,27 @@ -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.jenkins-ci.plugins</groupId> <artifactId>plugin</artifactId> - <version>3.2</version> + <version>4.15</version> + <relativePath /> </parent> <groupId>com.dubture.jenkins</groupId> <artifactId>digitalocean-plugin</artifactId> - <version>1.1.2-SNAPSHOT</version> + <version>${revision}${changelist}</version> <packaging>hpi</packaging> + <name>DigitalOcean plugin</name> <url>https://wiki.jenkins-ci.org/display/JENKINS/DigitalOcean+Plugin</url> - <properties> - <java.level>8</java.level> - </properties> + <licenses> + <license> + <name>The MIT License</name> + <url>https://opensource.org/licenses/MIT</url> + <distribution>repo</distribution> + </license> + </licenses> + <developers> <developer> <id>pulse00</id> @@ -37,54 +45,61 @@ <url>https://github.com/jenkinsci/digitalocean-plugin</url> <tag>HEAD</tag> </scm> - <licenses> - <license> - <name>MIT License</name> - <url>http://opensource.org/licenses/MIT</url> - </license> - </licenses> - <!-- get every artifact through repo.jenkins-ci.org, which proxies all the artifacts that we need --> - <repositories> - <repository> - <id>repo.jenkins-ci.org</id> - <url>https://repo.jenkins-ci.org/public/</url> - </repository> - <repository> - <id>oss-sonatype</id> - <url>http://oss.sonatype.org/content/repositories/snapshots/</url> - </repository> - </repositories> - <pluginRepositories> - <pluginRepository> - <id>repo.jenkins-ci.org</id> - <url>https://repo.jenkins-ci.org/public/</url> - </pluginRepository> - </pluginRepositories> + + <properties> + <revision>1.1.2</revision> + <changelist>-SNAPSHOT</changelist> + <jenkins.version>2.235.5</jenkins.version> + <java.level>8</java.level> + </properties> + <dependencies> <dependency> <groupId>com.myjeeva.digitalocean</groupId> <artifactId>digitalocean-api-client</artifactId> <version>2.17</version> </dependency> <dependency> - <groupId>com.google.guava</groupId> - <artifactId>guava</artifactId> - <version>15.0</version> + <groupId>org.jenkins-ci.plugins</groupId> + <artifactId>cloud-stats</artifactId> + <version>0.26</version> </dependency> <dependency> <groupId>org.jenkins-ci.plugins</groupId> - <artifactId>cloud-stats</artifactId> - <version>0.23</version> + <artifactId>credentials</artifactId> + </dependency> + <dependency> + <groupId>org.jenkins-ci.plugins</groupId> + <artifactId>ssh-credentials</artifactId> + </dependency> + <dependency> + <groupId>org.jenkins-ci.plugins</groupId> + <artifactId>plain-credentials</artifactId> </dependency> </dependencies> - <build> - <plugins> - <plugin> - <artifactId>maven-release-plugin</artifactId> - <configuration> - <goals>deploy</goals> - </configuration> - </plugin> - </plugins> - </build> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>io.jenkins.tools.bom</groupId> + <artifactId>bom-2.222.x</artifactId> + <version>10</version> + <scope>import</scope> + <type>pom</type> + </dependency> + </dependencies> + </dependencyManagement> + + <repositories> + <repository> + <id>repo.jenkins-ci.org</id> + <url>https://repo.jenkins-ci.org/public/</url> + </repository> + </repositories> + <pluginRepositories> + <pluginRepository> + <id>repo.jenkins-ci.org</id> + <url>https://repo.jenkins-ci.org/public/</url> + </pluginRepository> + </pluginRepositories> </project>
src/main/java/com/dubture/jenkins/digitalocean/DigitalOceanCloud.java+227 −19 modified@@ -33,31 +33,55 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.CheckForNull; + +import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.CredentialsStore; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.domains.DomainRequirement; import com.google.common.base.Strings; import com.myjeeva.digitalocean.exception.DigitalOceanException; import com.myjeeva.digitalocean.exception.RequestUnsuccessfulException; import com.myjeeva.digitalocean.impl.DigitalOceanClient; import com.myjeeva.digitalocean.pojo.Droplet; import com.myjeeva.digitalocean.pojo.Key; import com.thoughtworks.xstream.converters.UnmarshallingContext; + +import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; +import org.jenkinsci.plugins.cloudstats.TrackedPlannedNode; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; + +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.model.Computer; import hudson.model.Descriptor; import hudson.model.Label; import hudson.model.Node; +import hudson.security.ACL; import hudson.slaves.Cloud; import hudson.slaves.NodeProvisioner; import hudson.util.FormValidation; import hudson.util.ListBoxModel; +import hudson.util.Secret; import hudson.util.XStream2; import jenkins.model.Jenkins; -import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; -import org.jenkinsci.plugins.cloudstats.TrackedPlannedNode; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.QueryParameter; /** * The {@link DigitalOceanCloud} contains the main configuration values for running @@ -71,22 +95,32 @@ * @author robert.gruendler@dubture.com */ public class DigitalOceanCloud extends Cloud { + /** + * @See authTokenCredentialId + */ + @Deprecated + private transient String authToken; /** * The DigitalOcean API auth token * * @see "https://developers.digitalocean.com/documentation/v2/#authentication" */ - private final String authToken; + private String authTokenCredentialId; /** * The SSH key to be added to the new droplet. */ private final Integer sshKeyId; + /** + * @See privateKeyCredentialId + */ + @Deprecated + private transient String privateKey; /** * The SSH private key associated with the selected SSH key */ - private final String privateKey; + private String privateKeyCredentialId; private final Integer instanceCap; @@ -127,7 +161,7 @@ public class DigitalOceanCloud extends Cloud { * @param connectionRetryWait the time to wait for SSH connections to work * @param templates the templates for this cloud */ - @DataBoundConstructor + @Deprecated public DigitalOceanCloud(String name, String authToken, String privateKey, @@ -137,6 +171,30 @@ public DigitalOceanCloud(String name, String timeoutMinutes, String connectionRetryWait, List<? extends SlaveTemplate> templates) { + this(name, sshKeyId, instanceCap, usePrivateNetworking, timeoutMinutes, connectionRetryWait, templates); + } + + /** + * Constructor parameters are injected via jelly in the jenkins global configuration + * + * @param name A name associated with this cloud configuration + * @param authToken A DigitalOcean V2 API authentication token, generated on their website. + * @param privateKey An RSA private key in text format + * @param sshKeyId An identifier (name) for an SSH key known to DigitalOcean + * @param instanceCap the maximum number of instances that can be started + * @param usePrivateNetworking Whether to use private networking to connect to the cloud. + * @param timeoutMinutes the timeout in minutes. + * @param connectionRetryWait the time to wait for SSH connections to work + * @param templates the templates for this cloud + */ + @DataBoundConstructor + public DigitalOceanCloud(String name, + String sshKeyId, + String instanceCap, + Boolean usePrivateNetworking, + String timeoutMinutes, + String connectionRetryWait, + List<? extends SlaveTemplate> templates) { super(name); LOGGER.log(Level.INFO, "Constructing new DigitalOceanCloud(name = {0}, <token>, <privateKey>, <keyId>, instanceCap = {1}, ...)", new Object[]{name, instanceCap}); @@ -158,6 +216,29 @@ public DigitalOceanCloud(String name, LOGGER.info("Creating DigitalOcean cloud with " + this.templates.size() + " templates"); } + @DataBoundSetter + public void setPrivateKeyCredentialId(String credentialId) { + this.privateKeyCredentialId = credentialId; + } + + public String getPrivateKeyCredentialId() { + return this.privateKeyCredentialId; + } + + @DataBoundSetter + public void setAuthTokenCredentialId(String credentialId) { + this.authTokenCredentialId = credentialId; + } + + public String getAuthTokenCredentialId() { + return this.authTokenCredentialId; + } + + @Override + public String getDisplayName() { + return this.name; + } + private boolean isInstanceCapReachedLocal() { if (instanceCap == 0) { return false; @@ -219,10 +300,11 @@ private int getSlaveInstanceCap() { @Override public Collection<NodeProvisioner.PlannedNode> provision(final Label label, int excessWorkload) { synchronized (provisionSynchronizor) { + final String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); + final String privateKey = DigitalOceanCloud.getPrivateKeyFromCredentialId(privateKeyCredentialId); List<NodeProvisioner.PlannedNode> provisioningNodes = new ArrayList<>(); try { while (excessWorkload > 0) { - List<Droplet> droplets = DigitalOcean.getDroplets(authToken); if (isInstanceCapReachedLocal() || isInstanceCapReachedRemote(droplets)) { @@ -247,6 +329,7 @@ public Collection<NodeProvisioner.PlannedNode> provision(final Label label, int LOGGER.log(Level.INFO, "Instance cap reached, not provisioning."); return null; } + slave = template.provision(provisioningId, dropletName, name, authToken, privateKey, sshKeyId, droplets1, usePrivateNetworking); Jenkins.getInstance().addNode(slave); @@ -333,14 +416,33 @@ public String getName() { return name; } - public String getAuthToken() { - return authToken; + public static String getAuthTokenFromCredentialId(String credentialId) { + if (StringUtils.isBlank(credentialId)) { + return null; + } + StringCredentials cred = (StringCredentials) CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials(StringCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.emptyList()), + CredentialsMatchers.withId(credentialId)); + if (cred == null) { + return null; + } + return cred.getSecret().getPlainText(); } - public String getPrivateKey() { - return privateKey; + public static String getPrivateKeyFromCredentialId(String credentialId) { + if (StringUtils.isBlank(credentialId)) { + return null; + } + SSHUserPrivateKey cred = (SSHUserPrivateKey) CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials(SSHUserPrivateKey.class, Jenkins.get(), ACL.SYSTEM, Collections.emptyList()), + CredentialsMatchers.withId(credentialId)); + if (cred == null) { + return null; + } + return cred.getPrivateKeys().stream().findFirst().get(); } + public int getSshKeyId() { return sshKeyId; } @@ -350,6 +452,7 @@ public int getInstanceCap() { } public DigitalOceanClient getApiClient() { + final String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); return new DigitalOceanClient(authToken); } @@ -392,6 +495,87 @@ protected void callback(DigitalOceanCloud obj, UnmarshallingContext context) { } } + // borrowed from ec2-plugin + private void migratePrivateSshKeyToCredential(String privateKey){ + // GET matching private key credential from Credential API if exists + Optional<SSHUserPrivateKey> keyCredential = SystemCredentialsProvider.getInstance().getCredentials() + .stream() + .filter((cred) -> cred instanceof SSHUserPrivateKey) + .filter((cred) -> ((SSHUserPrivateKey)cred).getPrivateKey().trim().equals(privateKey.trim())) + .map(cred -> (SSHUserPrivateKey)cred) + .findFirst(); + + if (keyCredential.isPresent()){ + // SET this.sshKeysCredentialsId with the found credential + privateKeyCredentialId = keyCredential.get().getId(); + return; + } + + // CREATE new credential + String credsId = UUID.randomUUID().toString(); + + SSHUserPrivateKey sshKeyCredentials = new BasicSSHUserPrivateKey(CredentialsScope.SYSTEM, credsId, "key", + new BasicSSHUserPrivateKey.PrivateKeySource() { + @NonNull + @Override + public List<String> getPrivateKeys() { + return Collections.singletonList(privateKey.trim()); + } + }, "", "DigitalOcean Cloud Private Key - " + getDisplayName()); + + addNewGlobalCredential(sshKeyCredentials); + + privateKeyCredentialId = credsId; + } + + protected Object readResolve() { + if (this.privateKey != null ){ + migratePrivateSshKeyToCredential(this.privateKey); + } + this.privateKey = null; // This enforces it not to be persisted and that CasC will never output privateKey on export + + if (this.authToken != null) { + SystemCredentialsProvider systemCredentialsProvider = SystemCredentialsProvider.getInstance(); + + // ITERATE ON EXISTING CREDS AND DON'T CREATE IF EXIST + for (Credentials credentials: systemCredentialsProvider.getCredentials()) { + if (credentials instanceof StringCredentials) { + StringCredentials doCreds = (StringCredentials) credentials; + if (this.authToken.equals(doCreds.getSecret().toString())) { + this.authTokenCredentialId = doCreds.getId(); + this.authToken = null; + return this; + } + } + } + + // CREATE + String credsId = UUID.randomUUID().toString(); + addNewGlobalCredential(new StringCredentialsImpl(CredentialsScope.SYSTEM, credsId, "EC2 Cloud - " + getDisplayName(), Secret.fromString(this.authToken))); + this.authTokenCredentialId = credsId; + this.authToken = null; + + + // PROBLEM, GLOBAL STORE NOT FOUND + LOGGER.log(Level.WARNING, "DigitalOcean Plugin could not migrate credentials to the Jenkins Global Credentials Store, DigitalOcean Plugin for cloud {0} must be manually reconfigured", getDisplayName()); + } + + return this; + } + + private void addNewGlobalCredential(Credentials credentials){ + for (CredentialsStore credentialsStore: CredentialsProvider.lookupStores(Jenkins.get())) { + if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) { + try { + credentialsStore.addCredentials(Domain.global(), credentials); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Exception converting legacy configuration to the new credentials API", e); + } + } + + } + } + @Extension public static final class DescriptorImpl extends Descriptor<hudson.slaves.Cloud> { @@ -403,9 +587,9 @@ public String getDisplayName() { return "Digital Ocean"; } - public FormValidation doTestConnection(@QueryParameter String authToken) { + public FormValidation doTestConnection(@QueryParameter String authTokenCredentialId) { try { - DigitalOceanClient client = new DigitalOceanClient(authToken); + DigitalOceanClient client = new DigitalOceanClient(getAuthTokenFromCredentialId(authTokenCredentialId)); client.getAvailableDroplets(1, 10); return FormValidation.ok("Digital Ocean API request succeeded."); } catch (Exception e) { @@ -449,8 +633,8 @@ public FormValidation doCheckPrivateKey(@QueryParameter String value) throws IOE return FormValidation.ok(); } - public FormValidation doCheckSshKeyId(@QueryParameter String authToken) { - return doCheckAuthToken(authToken); + public FormValidation doCheckSshKeyId(@QueryParameter String authTokenCredentialId) { + return doCheckAuthToken(getAuthTokenFromCredentialId(authTokenCredentialId)); } public FormValidation doCheckInstanceCap(@QueryParameter String instanceCap) { @@ -473,20 +657,44 @@ public FormValidation doCheckInstanceCap(@QueryParameter String instanceCap) { } } - public ListBoxModel doFillSshKeyIdItems(@QueryParameter String authToken) throws RequestUnsuccessfulException, DigitalOceanException { + public ListBoxModel doFillSshKeyIdItems(@QueryParameter String authTokenCredentialId) throws RequestUnsuccessfulException, DigitalOceanException { ListBoxModel model = new ListBoxModel(); - if (authToken.isEmpty()) { + if (Strings.isNullOrEmpty(authTokenCredentialId)) { // Do not even attempt to list the keys if we know the authToken isn't going to work. // It only produces useless errors. return model; } - List<Key> availableSizes = DigitalOcean.getAvailableKeys(authToken); + List<Key> availableSizes = DigitalOcean.getAvailableKeys(getAuthTokenFromCredentialId(authTokenCredentialId)); for (Key key : availableSizes) { model.add(key.getName() + " (" + key.getFingerprint() + ")", key.getId().toString()); } return model; } + + public ListBoxModel doFillPrivateKeyCredentialIdItems(@QueryParameter String credentialsId) { + StandardListBoxModel result = new StandardListBoxModel(); + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + return result.includeCurrentValue(credentialsId); + } + return result + .includeEmptyValue() + .includeMatchingAs(Jenkins.getAuthentication(), Jenkins.get(), SSHUserPrivateKey.class, Collections.<DomainRequirement>emptyList(), CredentialsMatchers.always()) + .includeMatchingAs(ACL.SYSTEM, Jenkins.get(), SSHUserPrivateKey.class, Collections.<DomainRequirement>emptyList(), CredentialsMatchers.always()) + .includeCurrentValue(credentialsId); + } + + public ListBoxModel doFillAuthTokenCredentialIdItems(@QueryParameter String credentialsId) { + StandardListBoxModel result = new StandardListBoxModel(); + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + return result.includeCurrentValue(credentialsId); + } + return result + .includeEmptyValue() + .includeMatchingAs(Jenkins.getAuthentication(), Jenkins.get(), StringCredentials.class, Collections.<DomainRequirement>emptyList(), CredentialsMatchers.always()) + .includeMatchingAs(ACL.SYSTEM, Jenkins.get(), StringCredentials.class, Collections.<DomainRequirement>emptyList(), CredentialsMatchers.always()) + .includeCurrentValue(credentialsId); + } } }
src/main/java/com/dubture/jenkins/digitalocean/DigitalOceanComputer.java+4 −2 modified@@ -51,25 +51,27 @@ public class DigitalOceanComputer extends AbstractCloudComputer<Slave> implement private final ProvisioningActivity.Id provisioningId; - private final String authToken; + private final String authTokenCredentialId; private final Integer dropletId; public DigitalOceanComputer(Slave slave) { super(slave); provisioningId = slave.getId(); dropletId = slave.getDropletId(); - authToken = slave.getCloud().getAuthToken(); + authTokenCredentialId = slave.getCloud().getAuthTokenCredentialId(); } public Droplet updateInstanceDescription() throws RequestUnsuccessfulException, DigitalOceanException { + final String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); DigitalOceanClient apiClient = new DigitalOceanClient(authToken); return apiClient.getDropletInfo(dropletId); } @Override protected void onRemoved() { super.onRemoved(); + final String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); LOGGER.info("Slave removed, deleting droplet " + dropletId); DigitalOcean.tryDestroyDropletAsync(authToken, dropletId);
src/main/java/com/dubture/jenkins/digitalocean/DigitalOceanComputerLauncher.java+6 −5 modified@@ -184,11 +184,11 @@ public void launch(SlaveComputer _computer, TaskListener listener) { throw new Exception("Installing java failed."); } - logger.println("Copying slave.jar"); - scp.put(Jenkins.getInstance().getJnlpJars("slave.jar").readFully(), "slave.jar","/tmp"); + logger.println("Copying agent.jar"); + scp.put(Jenkins.getInstance().getJnlpJars("agent.jar").readFully(), "agent.jar","/tmp"); String jvmOpts = Util.fixNull(node.getJvmOpts()); - String launchString = "java " + jvmOpts + " -jar /tmp/slave.jar"; - logger.println("Launching slave agent: " + launchString); + String launchString = "java " + jvmOpts + " -jar /tmp/agent.jar"; + logger.println("Launching agent agent: " + launchString); final Session sess = conn.openSession(); sess.execCommand(launchString); digitalOceanComputer.setChannel(sess.getStdout(), sess.getStdin(), logger, new Channel.Listener() { @@ -304,8 +304,9 @@ private Connection connectToSsh(DigitalOceanComputer digitalOceanComputer, Print final Droplet droplet; try { + final String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(cloud.getAuthTokenCredentialId()); // Hack to fetch this each time through the loop to get the latest information. - droplet = DigitalOcean.getDroplet(cloud.getAuthToken(), node.getDropletId()); + droplet = DigitalOcean.getDroplet(authToken, node.getDropletId()); } catch (Exception e) { logger.println("Failed to get droplet. Retrying"); sleep(sleepTime);
src/main/java/com/dubture/jenkins/digitalocean/Slave.java+2 −1 modified@@ -147,7 +147,8 @@ public String getRemoteAdmin() { */ @Override protected void _terminate(TaskListener listener) throws IOException, InterruptedException { - DigitalOcean.tryDestroyDropletAsync(getCloud().getAuthToken(), dropletId); + final String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(getCloud().getAuthTokenCredentialId()); + DigitalOcean.tryDestroyDropletAsync(authToken, dropletId); } @Override
src/main/java/com/dubture/jenkins/digitalocean/SlaveTemplate.java+36 −20 modified@@ -35,6 +35,7 @@ import java.util.SortedMap; import java.util.logging.Level; import java.util.logging.Logger; + import javax.annotation.Nonnull; import com.google.common.base.Strings; @@ -46,6 +47,12 @@ import com.myjeeva.digitalocean.pojo.Key; import com.myjeeva.digitalocean.pojo.Region; import com.myjeeva.digitalocean.pojo.Size; + +import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + import hudson.Extension; import hudson.RelativePath; import hudson.Util; @@ -57,13 +64,6 @@ import hudson.util.FormValidation; import hudson.util.ListBoxModel; import jenkins.model.Jenkins; -import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.QueryParameter; - -import static java.lang.String.format; - -import static com.google.common.collect.Lists.newArrayList; /** * A {@link SlaveTemplate} represents the configuration values for creating a new slave via a DigitalOcean droplet. @@ -242,7 +242,7 @@ public Slave provision(ProvisioningActivity.Id provisioningId, new Object[]{imageId, sizeId, regionId}); if (isInstanceCapReachedLocal(cloudName) || isInstanceCapReachedRemote(droplets, cloudName)) { - String msg = format("instance cap reached for %s in %s", dropletName, cloudName); + String msg = String.format("instance cap reached for %s in %s", dropletName, cloudName); LOGGER.log(Level.INFO, msg); throw new AssertionError(msg); } @@ -258,7 +258,7 @@ public Slave provision(ProvisioningActivity.Id provisioningId, droplet.setSize(sizeId); droplet.setRegion(new Region(regionId)); droplet.setImage(DigitalOcean.newImage(imageId)); - droplet.setKeys(newArrayList(new Key(sshKeyId))); + droplet.setKeys(Arrays.asList(new Key(sshKeyId))); droplet.setInstallMonitoring(installMonitoringAgent); droplet.setEnablePrivateNetworking( (usePrivateNetworking == null ? false : usePrivateNetworking) || (setupPrivateNetworking == null ? false : setupPrivateNetworking) @@ -278,7 +278,7 @@ public Slave provision(ProvisioningActivity.Id provisioningId, } catch (RuntimeException e) { throw e; } catch (Exception e) { - String msg = format("Unexpected error raised during provisioning of %s:%n%s", dropletName, e.getMessage()); + String msg = String.format("Unexpected error raised during provisioning of %s:%n%s", dropletName, e.getMessage()); LOGGER.log(Level.WARNING, msg, e); throw new AssertionError(msg); } @@ -411,22 +411,29 @@ public FormValidation doCheckInstanceCap(@QueryParameter String instanceCap) { return doCheckNonNegativeNumber(instanceCap); } - public FormValidation doCheckSizeId(@RelativePath("..") @QueryParameter String authToken) { + public FormValidation doCheckSizeId(@RelativePath("..") @QueryParameter String authTokenCredentialId) { + String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); return DigitalOceanCloud.DescriptorImpl.doCheckAuthToken(authToken); } - public FormValidation doCheckImageId(@RelativePath("..") @QueryParameter String authToken) { + public FormValidation doCheckImageId(@RelativePath("..") @QueryParameter String authTokenCredentialId) { + String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); return DigitalOceanCloud.DescriptorImpl.doCheckAuthToken(authToken); } - public FormValidation doCheckRegionId(@RelativePath("..") @QueryParameter String authToken) { + public FormValidation doCheckRegionId(@RelativePath("..") @QueryParameter String authTokenCredentialId) { + String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); return DigitalOceanCloud.DescriptorImpl.doCheckAuthToken(authToken); } - public ListBoxModel doFillSizeIdItems(@RelativePath("..") @QueryParameter String authToken) throws Exception { + public ListBoxModel doFillSizeIdItems(@RelativePath("..") @QueryParameter String authTokenCredentialId) throws Exception { + ListBoxModel model = new ListBoxModel(); + String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); + if (StringUtils.isBlank(authToken)) { + return model; + } List<Size> availableSizes = DigitalOcean.getAvailableSizes(authToken); - ListBoxModel model = new ListBoxModel(); for (Size size : availableSizes) { model.add(DigitalOcean.buildSizeLabel(size), size.getSlug()); @@ -435,10 +442,15 @@ public ListBoxModel doFillSizeIdItems(@RelativePath("..") @QueryParameter String return model; } - public ListBoxModel doFillImageIdItems(@RelativePath("..") @QueryParameter String authToken) throws Exception { + public ListBoxModel doFillImageIdItems(@RelativePath("..") @QueryParameter String authTokenCredentialId) throws Exception { - SortedMap<String, Image> availableImages = DigitalOcean.getAvailableImages(authToken); ListBoxModel model = new ListBoxModel(); + String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); + if (StringUtils.isBlank(authToken)) { + return model; + } + + SortedMap<String, Image> availableImages = DigitalOcean.getAvailableImages(DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId)); for (Map.Entry<String, Image> entry : availableImages.entrySet()) { final Image image = entry.getValue(); @@ -453,10 +465,14 @@ public ListBoxModel doFillImageIdItems(@RelativePath("..") @QueryParameter Strin return model; } - public ListBoxModel doFillRegionIdItems(@RelativePath("..") @QueryParameter String authToken) throws Exception { - - List<Region> availableSizes = DigitalOcean.getAvailableRegions(authToken); + public ListBoxModel doFillRegionIdItems(@RelativePath("..") @QueryParameter String authTokenCredentialId) throws Exception { ListBoxModel model = new ListBoxModel(); + String authToken = DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId); + if (StringUtils.isBlank(authToken)) { + return model; + } + + List<Region> availableSizes = DigitalOcean.getAvailableRegions(DigitalOceanCloud.getAuthTokenFromCredentialId(authTokenCredentialId)); for (Region region : availableSizes) { model.add(region.getName(), region.getSlug());
src/main/resources/com/dubture/jenkins/digitalocean/DigitalOceanCloud/config.jelly+4 −4 modified@@ -32,16 +32,16 @@ <f:textbox/> </f:entry> - <f:entry title="Auth token" field="authToken"> - <f:password /> + <f:entry title="${%Auth token}" field="authTokenCredentialId"> + <f:select /> </f:entry> <f:entry title="SSH public key" field="sshKeyId"> <f:select /> </f:entry> - <f:entry title="SSH private key" field="privateKey"> - <f:textarea/> + <f:entry title="${%SSH private key}" field="privateKeyCredentialId"> + <f:select /> </f:entry> <f:entry title="Instance cap" field="instanceCap">
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-8g6v-g8qc-5w7jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-2126ghsaADVISORY
- www.openwall.com/lists/oss-security/2020/02/12/3ghsamailing-listx_refsource_MLISTWEB
- github.com/jenkinsci/digitalocean-plugin/commit/ca1112c5c52a842c6340401f02f536fc07c6620aghsaWEB
- jenkins.io/security/advisory/2020-02-12/ghsax_refsource_CONFIRMWEB
News mentions
1- Jenkins Security Advisory 2020-02-12Jenkins Security Advisories · Feb 12, 2020