CVE-2019-10341
Description
Jenkins Docker Plugin 1.1.6 and earlier lacked a permission check in doTestConnection, allowing users with Overall/Read to capture stored credentials via attacker-specified URLs.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins Docker Plugin 1.1.6 and earlier lacked a permission check in doTestConnection, allowing users with Overall/Read to capture stored credentials via attacker-specified URLs.
Vulnerability
Jenkins Docker Plugin versions 1.1.6 and earlier contain a missing permission check in the DockerAPI.DescriptorImpl#doTestConnection method. This method performs form validation but does not verify that the user has appropriate permissions (e.g., Overall/Administer or Item/Configure). Additionally, the method does not require POST requests, making it also vulnerable to cross-site request forgery (CVE-2019-10340). Affected versions: all up to and including 1.1.6 [2].
Exploitation
An attacker with Overall/Read access to Jenkins can exploit this vulnerability. First, the attacker must obtain valid credential IDs, which can be enumerated via a separate vulnerability (CVE-2019-10342) or through other means. The attacker then crafts a request to doTestConnection with an attacker-specified URL and credential ID. Because no permission check is performed, the method executes and connects to the attacker-controlled server using the specified credentials, causing Jenkins to transmit the credentials to the attacker [2].
Impact
Successful exploitation leads to the capture of credentials stored in Jenkins. The attacker gains access to these credentials, potentially allowing further compromise of Jenkins and connected systems. The primary impact is unauthorized credential disclosure, affecting confidentiality and potentially integrity and availability depending on the captured credentials [2].
Mitigation
The vulnerability is fixed in Docker Plugin version 1.1.7, released on July 11, 2019. The fix adds a permission check requiring Overall/Administer or Item/Configure and enforces POST requests via @RequirePOST. Users should upgrade to 1.1.7 or later [3][4]. No other workarounds are documented.
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.jenkins.docker:docker-pluginMaven | < 1.1.7 | 1.1.7 |
Affected products
2- Range: 1.1.6 and earlier
Patches
16ad27199f6fa[SECURITY-1010]
8 files changed · +122 −134
src/main/java/com/nirima/jenkins/plugins/docker/builder/DockerBuilderNewTemplate.java+0 −24 modified@@ -1,27 +1,16 @@ package com.nirima.jenkins.plugins.docker.builder; -import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator; -import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserListBoxModel; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; import com.nirima.jenkins.plugins.docker.DockerCloud; import com.nirima.jenkins.plugins.docker.DockerTemplate; -import com.trilead.ssh2.Connection; import hudson.Extension; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; -import hudson.model.ItemGroup; -import hudson.plugins.sshslaves.SSHLauncher; -import hudson.security.ACL; -import hudson.security.AccessControlled; import hudson.slaves.Cloud; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; -import hudson.util.ListBoxModel; import jenkins.model.Jenkins; -import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,7 +74,6 @@ public DescriptorImpl getDescriptor() { @Extension public static class DescriptorImpl extends BuildStepDescriptor<Builder> { - @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; @@ -95,17 +83,5 @@ public boolean isApplicable(Class<? extends AbstractProject> jobType) { public String getDisplayName() { return "Add a new template to all docker clouds"; } - - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context) { - - AccessControlled ac = (context instanceof AccessControlled ? (AccessControlled) context : Jenkins.getInstance()); - if (!ac.hasPermission(Jenkins.ADMINISTER)) { - return new ListBoxModel(); - } - - return new SSHUserListBoxModel().withMatching(SSHAuthenticator.matcher(Connection.class), - CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, context, - ACL.SYSTEM, SSHLauncher.SSH_SCHEME)); - } } }
src/main/java/com/nirima/jenkins/plugins/docker/DockerManagementServer.java+13 −5 modified@@ -11,19 +11,21 @@ import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.interceptor.RequirePOST; -import javax.servlet.ServletException; import java.io.IOException; import java.util.Collection; +import java.util.Collections; import java.util.Date; /** * Created by magnayn on 22/02/2014. */ -public class DockerManagementServer implements Describable<DockerManagementServer> { +public class DockerManagementServer implements Describable<DockerManagementServer> { final String name; final DockerCloud theCloud; + @Override public Descriptor<DockerManagementServer> getDescriptor() { return Jenkins.getInstance().getDescriptorByType(DescriptorImpl.class); } @@ -38,6 +40,9 @@ public DockerManagementServer(String name) { } public Collection getImages(){ + if ( !Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER) ) { + return Collections.emptyList(); + } final DockerAPI dockerApi = theCloud.getDockerApi(); try(final DockerClient client = dockerApi.getClient()) { return client.listImagesCmd().exec(); @@ -47,6 +52,9 @@ public Collection getImages(){ } public Collection getProcesses() { + if ( !Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER) ) { + return Collections.emptyList(); + } final DockerAPI dockerApi = theCloud.getDockerApi(); try(final DockerClient client = dockerApi.getClient()) { return client.listContainersCmd().exec(); @@ -67,9 +75,9 @@ public String getJsUrl(String jsName) { return Consts.PLUGIN_JS_URL + jsName; } - public void doControlSubmit(@QueryParameter("stopId") String stopId, StaplerRequest req, StaplerResponse rsp) throws ServletException, - IOException, - InterruptedException { + @RequirePOST + public void doControlSubmit(@QueryParameter("stopId") String stopId, StaplerRequest req, StaplerResponse rsp) throws IOException { + Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); final DockerAPI dockerApi = theCloud.getDockerApi(); try(final DockerClient client = dockerApi.getClient()) { client.stopContainerCmd(stopId).exec();
src/main/java/com/nirima/jenkins/plugins/docker/DockerSimpleTemplate.java+0 −14 modified@@ -1,11 +1,7 @@ package com.nirima.jenkins.plugins.docker; import hudson.Extension; -import hudson.model.Item; -import hudson.util.ListBoxModel; import jenkins.model.Jenkins; -import org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint; -import org.kohsuke.stapler.AncestorInPath; /** * A simple template storage. @@ -58,19 +54,9 @@ public DescriptorImpl getDescriptor() { @Extension public static final class DescriptorImpl extends DockerTemplateBase.DescriptorImpl { - @Override public String getDisplayName() { return "Docker Template"; } - - public ListBoxModel doFillPullCredentialsIdItems(@AncestorInPath Item item) { - final DockerRegistryEndpoint.DescriptorImpl descriptor = - (DockerRegistryEndpoint.DescriptorImpl) - Jenkins.getInstance().getDescriptorOrDie(DockerRegistryEndpoint.class); - return descriptor.doFillCredentialsIdItems(item); - } - - } }
src/main/java/com/nirima/jenkins/plugins/docker/DockerTemplateBase.java+2 −35 modified@@ -1,9 +1,5 @@ package com.nirima.jenkins.plugins.docker; -import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator; -import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserListBoxModel; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.PortBinding; @@ -17,16 +13,11 @@ import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.nirima.jenkins.plugins.docker.utils.JenkinsUtils; -import com.trilead.ssh2.Connection; import hudson.Extension; import hudson.Util; import hudson.model.Describable; import hudson.model.Descriptor; import hudson.model.Item; -import hudson.model.ItemGroup; -import hudson.plugins.sshslaves.SSHLauncher; -import hudson.security.ACL; -import hudson.security.AccessControlled; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import jenkins.model.Jenkins; @@ -742,9 +733,7 @@ public FormValidation doCheckVolumesString(@QueryParameter String volumesString) } catch (Throwable t) { return FormValidation.error(t.getMessage()); } - return FormValidation.ok(); - } public FormValidation doCheckVolumesFromString(@QueryParameter String volumesFromString) { @@ -756,7 +745,6 @@ public FormValidation doCheckVolumesFromString(@QueryParameter String volumesFro } catch (Throwable t) { return FormValidation.error(t.getMessage()); } - return FormValidation.ok(); } @@ -767,40 +755,19 @@ public FormValidation doCheckExtraHostsString(@QueryParameter String extraHostsS return FormValidation.error("Wrong extraHost {}", extraHost); } } - return FormValidation.ok(); } - - public ListBoxModel doFillPullCredentialsIdItems(@AncestorInPath Item item) { + public ListBoxModel doFillPullCredentialsIdItems(@AncestorInPath Item context) { final DockerRegistryEndpoint.DescriptorImpl descriptor = (DockerRegistryEndpoint.DescriptorImpl) Jenkins.getInstance().getDescriptorOrDie(DockerRegistryEndpoint.class); - return descriptor.doFillCredentialsIdItems(item); - } - - - public static ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context) { - - AccessControlled ac = (context instanceof AccessControlled ? (AccessControlled) context : Jenkins.getInstance()); - if (!ac.hasPermission(Jenkins.ADMINISTER)) { - return new ListBoxModel(); - } - - return new SSHUserListBoxModel().withMatching( - SSHAuthenticator.matcher(Connection.class), - CredentialsProvider.lookupCredentials( - StandardUsernameCredentials.class, - context, - ACL.SYSTEM, - SSHLauncher.SSH_SCHEME) - ); + return descriptor.doFillCredentialsIdItems(context); } @Override public String getDisplayName() { return "Docker template base"; } } - }
src/main/java/io/jenkins/docker/client/DockerAPI.java+47 −10 modified@@ -1,6 +1,5 @@ package io.jenkins.docker.client; -import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.domains.DomainRequirement; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.VersionCmd; @@ -12,9 +11,8 @@ import hudson.Extension; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; -import hudson.model.ItemGroup; +import hudson.model.Item; import hudson.security.ACL; -import hudson.security.AccessControlled; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import jenkins.model.Jenkins; @@ -24,6 +22,7 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; import org.newsclub.net.unix.AFUNIXSocket; import org.newsclub.net.unix.AFUNIXSocketAddress; import org.slf4j.Logger; @@ -318,14 +317,17 @@ public int hashCode() { @Extension public static class DescriptorImpl extends Descriptor<DockerAPI> { - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context, @QueryParameter String value) { - AccessControlled ac = (context instanceof AccessControlled ? (AccessControlled) context : Jenkins.getInstance()); - if (!ac.hasPermission(Jenkins.ADMINISTER)) { - return new StandardListBoxModel().includeCurrentValue(value); + public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item context, @QueryParameter String uri) { + final DockerServerEndpoint.DescriptorImpl descriptor = (DockerServerEndpoint.DescriptorImpl) Jenkins.getInstance().getDescriptorOrDie(DockerServerEndpoint.class); + return descriptor.doFillCredentialsIdItems(context, uri); + } + + public FormValidation doCheckCredentialsId(@AncestorInPath Item context, @QueryParameter String uri, @QueryParameter String value) { + final String credentialsOrNull = trimToNull(value); + if ( credentialsOrNull==null || credentialsAreValid(context, uri, credentialsOrNull)) { + return FormValidation.ok(); } - return new StandardListBoxModel().includeAs( - ACL.SYSTEM, context, DockerServerCredentials.class, - Collections.<DomainRequirement>emptyList()); + return FormValidation.error("Invalid credentials for URI " + uri); } public FormValidation doCheckConnectionTimeout(@QueryParameter String value) { @@ -336,13 +338,20 @@ public FormValidation doCheckReadTimeout(@QueryParameter String value) { return FormValidation.validateNonNegativeInteger(value); } + @RequirePOST public FormValidation doTestConnection( + @AncestorInPath Item context, @QueryParameter String uri, @QueryParameter String credentialsId, @QueryParameter String apiVersion, @QueryParameter int connectTimeout, @QueryParameter int readTimeout ) { + throwIfNoPermission(context); + final FormValidation credentialsIdCheckResult = doCheckCredentialsId(context, uri, credentialsId); + if (credentialsIdCheckResult != FormValidation.ok()) { + return FormValidation.error("Invalid credentials"); + } try { final DockerServerEndpoint dsep = new DockerServerEndpoint(uri, credentialsId); final DockerAPI dapi = new DockerAPI(dsep, connectTimeout, readTimeout, apiVersion, null); @@ -357,5 +366,33 @@ public FormValidation doTestConnection( return FormValidation.error(e, e.getMessage()); } } + + private boolean credentialsAreValid(Item context, String uri, final String credentialsId) { + final ListBoxModel availableCredentials = doFillCredentialsIdItems(context, uri); + return optionIsAvailable(credentialsId, availableCredentials); + } + + private boolean optionIsAvailable(final String optionValue, final ListBoxModel available) { + for (ListBoxModel.Option o : available) { + if (o.value == null) { + if (optionValue == null) { + return true; // both null = match + } + } else { + if (optionValue != null && optionValue.equals(o.value)) { + return true; + } + } + } + return false; + } + + private void throwIfNoPermission(Item context) { + if (context != null) { + context.checkPermission(Item.CONFIGURE); + } else { + Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); + } + } } }
src/main/java/io/jenkins/docker/connector/DockerComputerSSHConnector.java+29 −4 modified@@ -1,26 +1,29 @@ package io.jenkins.docker.connector; +import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator; import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; +import com.cloudbees.plugins.credentials.common.StandardUsernameListBoxModel; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.NetworkSettings; import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; -import com.nirima.jenkins.plugins.docker.DockerTemplateBase; import com.nirima.jenkins.plugins.docker.utils.PortUtils; +import com.trilead.ssh2.Connection; import com.trilead.ssh2.signature.RSAKeyAlgorithm; import hudson.Extension; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; -import hudson.model.ItemGroup; +import hudson.model.Item; import hudson.model.TaskListener; import hudson.plugins.sshslaves.SSHLauncher; import hudson.plugins.sshslaves.verifiers.NonVerifyingKeyVerificationStrategy; import hudson.plugins.sshslaves.verifiers.SshHostKeyVerificationStrategy; +import hudson.security.ACL; import hudson.slaves.ComputerLauncher; import hudson.util.ListBoxModel; import io.jenkins.docker.client.DockerAPI; @@ -33,6 +36,7 @@ import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +48,7 @@ import java.io.InputStream; import java.net.InetSocketAddress; import java.net.URI; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -419,8 +424,28 @@ public String getDisplayName() { return "Use configured SSH credentials"; } - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context) { - return DockerTemplateBase.DescriptorImpl.doFillCredentialsIdItems(context); + public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item context, @QueryParameter String credentialsId) { + if ( !hasPermission(context)) { + return new StandardUsernameListBoxModel() + .includeCurrentValue(credentialsId); + } + // Functionally the same as SSHLauncher's descriptor method, but without + // filtering by host/port as we don't/can't know those yet. + return new StandardUsernameListBoxModel() + .includeMatchingAs( + ACL.SYSTEM, + context, + StandardUsernameCredentials.class, + Collections.emptyList(), + SSHAuthenticator.matcher(Connection.class)) + .includeCurrentValue(credentialsId); + } + + private boolean hasPermission(Item context) { + if (context != null) { + return context.hasPermission(Item.CONFIGURE); + } + return Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER); } } }
src/main/java/io/jenkins/docker/pipeline/DockerNodeStep.java+1 −1 modified@@ -106,7 +106,7 @@ public String getDisplayName() { } public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item item, @QueryParameter String uri) { - DockerServerEndpoint.DescriptorImpl descriptor = (DockerServerEndpoint.DescriptorImpl) Jenkins.getInstance().getDescriptor(DockerServerEndpoint.class); + DockerServerEndpoint.DescriptorImpl descriptor = (DockerServerEndpoint.DescriptorImpl) Jenkins.getInstance().getDescriptorOrDie(DockerServerEndpoint.class); return descriptor.doFillCredentialsIdItems(item, uri); }
src/main/resources/com/nirima/jenkins/plugins/docker/DockerManagementServer/index.jelly+30 −41 modified@@ -26,44 +26,38 @@ <form method="post" action="controlSubmit" name="controlSubmit" id="control"> <input type="hidden" id="stopId" name="stopId" value=""/> - <table width="100%" border="1" cellpadding="2" cellspacing="0" - class="pane bigtable" - style="margin-top: 0"> - <tr> - - <td class="pane-header">${%Container Id}</td> - <td class="pane-header">${%Image}</td> - <td class="pane-header">${%Command}</td> - <td class="pane-header">${%Created}</td> - <td class="pane-header">${%Status}</td> - <td class="pane-header">${%Ports}</td> - <td> - </td> - - </tr> - - <j:forEach var="res" items="${it.processes}"> + <table width="100%" border="1" cellpadding="2" cellspacing="0" + class="pane bigtable" + style="margin-top: 0"> <tr> - <td>${res.id}</td> - <td>${res.image}</td> - <td>${res.command}</td> - <td>${it.asTime(res.created)}</td> - <td>${res.status}</td> - <td> - <j:forEach var="port" items="${res.ports}"> - <p>${port}</p> - <br/> - </j:forEach> - </td> - - <td> - <input type="button" value="stop" onclick="stop('${res.id}')"></input> - </td> - + <td class="pane-header">${%Container Id}</td> + <td class="pane-header">${%Image}</td> + <td class="pane-header">${%Command}</td> + <td class="pane-header">${%Created}</td> + <td class="pane-header">${%Status}</td> + <td class="pane-header">${%Ports}</td> + <td> - </td> </tr> - </j:forEach> - - - </table> + <j:forEach var="res" items="${it.processes}"> + <tr> + <td>${res.id}</td> + <td>${res.image}</td> + <td>${res.command}</td> + <td>${it.asTime(res.created)}</td> + <td>${res.status}</td> + <td> + <j:forEach var="port" items="${res.ports}"> + <p>${port}</p> + <br/> + </j:forEach> + </td> + <td> + <input type="button" value="stop" onclick="stop('${res.id}')"></input> + </td> + </tr> + </j:forEach> + </table> + </form> <H2>Images</H2> @@ -77,20 +71,15 @@ <td class="pane-header">${%Created}</td> <td class="pane-header">${%Virtual Size}</td> </tr> - - <j:forEach var="res" items="${it.images}"> <tr> - <td>${res.tag}</td> <td>${res.id}</td> <td>${it.asTime(res.created)}</td> <td>${res.virtualSize}</td> </tr> </j:forEach> - </table> - </form> </l:main-panel> </l:layout>
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-76w6-m7vv-7hhwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2019-10341ghsaADVISORY
- www.openwall.com/lists/oss-security/2019/07/11/4ghsamailing-listx_refsource_MLISTWEB
- www.securityfocus.com/bid/109156ghsavdb-entryx_refsource_BIDWEB
- github.com/jenkinsci/docker-plugin/commit/6ad27199f6fad230be72fd45da78ddac85c075dbghsaWEB
- jenkins.io/security/advisory/2019-07-11/ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.