CVE-2017-1000393
Description
In Jenkins 2.73.1 and earlier, and 2.83 and earlier, users with agent create/configure permission could run arbitrary commands on the master via the 'Launch agent via execution of command on master' launch method.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
In Jenkins 2.73.1 and earlier, and 2.83 and earlier, users with agent create/configure permission could run arbitrary commands on the master via the 'Launch agent via execution of command on master' launch method.
Vulnerability
Jenkins versions 2.73.1 and earlier, as well as 2.83 and earlier, contained a privilege escalation vulnerability in the agent configuration functionality. Users who had permission to create or configure agents could set the launch method to Launch agent via execution of command on master (CommandLauncher). This launch method allowed them to specify an arbitrary shell command that would be executed on the Jenkins master node whenever the agent was supposed to be launched. The vulnerability is tracked as SECURITY-478 [1][2][3].
Exploitation
An attacker needs to have the Agent/Create or Agent/Configure permission in Jenkins. With this access, they can create a new agent or modify an existing agent and set its launch method to Launch agent via execution of command on master. In the configuration, they can enter any arbitrary shell command. When the agent is launched (e.g., triggered manually or by a build), the specified command executes on the master node. No additional user interaction is required beyond the attacker's configuration change [1][3].
Impact
Successful exploitation allows an attacker to execute arbitrary shell commands on the Jenkins master node. This can lead to full compromise of the Jenkins server, including data exfiltration, installation of malware, or lateral movement within the network. The attacker effectively gains the ability to run privileged commands, bypassing the intended restriction that only administrators should be able to execute arbitrary scripts on the master [2][3].
Mitigation
Jenkins released fixes in versions 2.74 and 2.84. The fix requires the Run Scripts permission (typically granted only to administrators) to configure the CommandLauncher launch method. Users should upgrade to Jenkins 2.74 or later (or 2.84 or later for the LTS line). As a workaround, administrators can review and restrict the Agent/Create and Agent/Configure permissions to trusted users only, though this does not fully address the vulnerability. The advisory notes a known limitation that users without Run Scripts permission can no longer configure any agent with this launch method, even if unchanged; a future plugin separation is planned but not yet released [3].
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 |
|---|---|---|
org.jenkins-ci.main:jenkins-coreMaven | < 2.73.2 | 2.73.2 |
org.jenkins-ci.main:jenkins-coreMaven | >= 2.74, < 2.84 | 2.84 |
Affected products
1Patches
267f68c181033[SECURITY-478] Strengthening test.
1 file changed · +18 −1
test/src/test/java/hudson/slaves/CommandLauncher2Test.java+18 −1 modified@@ -29,15 +29,19 @@ import com.gargoylesoftware.htmlunit.WebRequest; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlTextInput; +import hudson.XmlFile; import hudson.cli.CLICommand; import hudson.cli.CLICommandInvoker; import hudson.cli.UpdateNodeCommand; import hudson.model.Computer; import hudson.model.User; +import java.io.File; +import java.io.IOException; import java.net.HttpURLConnection; +import javax.annotation.CheckForNull; import jenkins.model.Jenkins; import org.apache.tools.ant.filters.StringInputStream; -import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.*; import org.junit.Test; import static org.junit.Assert.*; import org.junit.Rule; @@ -73,6 +77,7 @@ public void evaluate() throws Throwable { rr.j.submit(form); s = (DumbSlave) rr.j.jenkins.getNode("s"); assertEquals("echo configured by GUI", ((CommandLauncher) s.getLauncher()).getCommand()); + assertSerialForm(s, "echo configured by GUI"); // Then by REST. String configDotXml = s.toComputer().getUrl() + "config.xml"; String xml = wc.goTo(configDotXml, "application/xml").getWebResponse().getContentAsString(); @@ -83,14 +88,17 @@ public void evaluate() throws Throwable { wc.getPage(req); s = (DumbSlave) rr.j.jenkins.getNode("s"); assertEquals("echo configured by REST", ((CommandLauncher) s.getLauncher()).getCommand()); + assertSerialForm(s, "echo configured by REST"); // Then by CLI. CLICommand cmd = new UpdateNodeCommand(); cmd.setTransportAuth(User.get("admin").impersonate()); assertThat(new CLICommandInvoker(rr.j, cmd).withStdin(new StringInputStream(xml.replace("echo configured by GUI", "echo configured by CLI"))).invokeWithArgs("s"), CLICommandInvoker.Matcher.succeededSilently()); s = (DumbSlave) rr.j.jenkins.getNode("s"); assertEquals("echo configured by CLI", ((CommandLauncher) s.getLauncher()).getCommand()); + assertSerialForm(s, "echo configured by CLI"); // Now verify that all modes failed as dev. First as GUI. s.setLauncher(new CommandLauncher("echo configured by admin")); + s.save(); wc = rr.j.createWebClient().login("dev"); form = wc.getPage(s, "configure").getFormByName("config"); input = form.getInputByName("_.command"); @@ -104,6 +112,7 @@ public void evaluate() throws Throwable { } s = (DumbSlave) rr.j.jenkins.getNode("s"); assertEquals("echo configured by admin", ((CommandLauncher) s.getLauncher()).getCommand()); + assertSerialForm(s, "echo configured by admin"); // Then by REST. req = new WebRequest(wc.createCrumbedUrl(configDotXml), HttpMethod.POST); req.setEncodingType(null); @@ -115,6 +124,7 @@ public void evaluate() throws Throwable { } s = (DumbSlave) rr.j.jenkins.getNode("s"); assertNotEquals(CommandLauncher.class, s.getLauncher().getClass()); // currently seems to reset it to JNLPLauncher, whatever + assertSerialForm(s, null); s.setLauncher(new CommandLauncher("echo configured by admin")); // Then by CLI. cmd = new UpdateNodeCommand(); @@ -123,10 +133,17 @@ public void evaluate() throws Throwable { CLICommandInvoker.Matcher./* gets swallowed by RobustReflectionConverter, hmm*/succeededSilently()); s = (DumbSlave) rr.j.jenkins.getNode("s"); assertNotEquals(CommandLauncher.class, s.getLauncher().getClass()); + assertSerialForm(s, null); // Now also check that SYSTEM deserialization works after a restart. s.setLauncher(new CommandLauncher("echo configured by admin")); s.save(); } + private void assertSerialForm(DumbSlave s, @CheckForNull String expectedCommand) throws IOException { + // cf. private methods in Nodes + File nodesDir = new File(rr.j.jenkins.getRootDir(), "nodes"); + XmlFile configXml = new XmlFile(Jenkins.XSTREAM, new File(new File(nodesDir, s.getNodeName()), "config.xml")); + assertThat(configXml.asString(), expectedCommand != null ? containsString("<agentCommand>" + expectedCommand + "</agentCommand>") : not(containsString("<agentCommand>"))); + } }); rr.addStep(new Statement() { @Override
d7ea3f40efed[SECURITY-478] Require RUN_SCRIPTS before configuring CommandLauncher or CommandConnector.
5 files changed · +183 −12
core/src/main/java/hudson/slaves/CommandConnector.java+22 −0 modified@@ -25,11 +25,15 @@ import hudson.EnvVars; import hudson.Extension; +import hudson.Util; import hudson.model.TaskListener; +import hudson.util.FormValidation; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; import java.io.IOException; +import jenkins.model.Jenkins; +import org.kohsuke.stapler.QueryParameter; /** * Executes a program on the master and expect that script to connect. @@ -42,6 +46,12 @@ public class CommandConnector extends ComputerConnector { @DataBoundConstructor public CommandConnector(String command) { this.command = command; + Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); + } + + private Object readResolve() { + Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); + return this; } @Override @@ -55,5 +65,17 @@ public static class DescriptorImpl extends ComputerConnectorDescriptor { public String getDisplayName() { return Messages.CommandLauncher_displayName(); } + + public FormValidation doCheckCommand(@QueryParameter String value) { + if (!Jenkins.getInstance().hasPermission(Jenkins.RUN_SCRIPTS)) { + return FormValidation.warning(Messages.CommandLauncher_cannot_be_configured_by_non_administrato()); + } + if (Util.fixEmptyAndTrim(value) == null) { + return FormValidation.error(Messages.CommandLauncher_NoLaunchCommand()); + } else { + return FormValidation.ok(); + } + } + } }
core/src/main/java/hudson/slaves/CommandLauncher.java+10 −0 modified@@ -33,6 +33,7 @@ import jenkins.model.Jenkins; import hudson.model.TaskListener; import hudson.remoting.Channel; +import hudson.security.ACL; import hudson.util.StreamCopyThread; import hudson.util.FormValidation; import hudson.util.ProcessTree; @@ -74,6 +75,12 @@ public CommandLauncher(String command) { public CommandLauncher(String command, EnvVars env) { this.agentCommand = command; this.env = env; + Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); + } + + private Object readResolve() { + Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); + return this; } public String getCommand() { @@ -191,6 +198,9 @@ public String getDisplayName() { } public FormValidation doCheckCommand(@QueryParameter String value) { + if (!Jenkins.getInstance().hasPermission(Jenkins.RUN_SCRIPTS)) { + return FormValidation.warning(Messages.CommandLauncher_cannot_be_configured_by_non_administrato()); + } if(Util.fixEmptyAndTrim(value)==null) return FormValidation.error(Messages.CommandLauncher_NoLaunchCommand()); else
core/src/main/resources/hudson/slaves/Messages.properties+1 −0 modified@@ -27,6 +27,7 @@ CommandLauncher.displayName=Launch agent via execution of command on the master JNLPLauncher.displayName=Launch agent via Java Web Start ComputerLauncher.unexpectedError=Unexpected error in launching an agent. This is probably a bug in Jenkins ComputerLauncher.abortedLaunch=Launching agent process aborted. +CommandLauncher.cannot_be_configured_by_non_administrato=cannot be configured by non-administrators CommandLauncher.NoLaunchCommand=No launch command specified ConnectionActivityMonitor.OfflineCause=Repeated ping attempts failed DumbSlave.displayName=Permanent Agent
test/src/test/java/hudson/model/ProjectTest.java+10 −12 modified@@ -27,7 +27,6 @@ import com.gargoylesoftware.htmlunit.WebRequest; import hudson.model.queue.QueueTaskFuture; import hudson.security.AccessDeniedException2; -import org.acegisecurity.context.SecurityContextHolder; import hudson.security.HudsonPrivateSecurityRealm; import hudson.security.GlobalMatrixAuthorizationStrategy; @@ -67,6 +66,8 @@ import hudson.EnvVars; import hudson.model.labels.LabelAtom; import hudson.scm.SCMDescriptor; +import hudson.security.ACL; +import hudson.security.ACLContext; import hudson.slaves.Cloud; import hudson.slaves.DumbSlave; import hudson.slaves.NodeProvisioner; @@ -537,8 +538,7 @@ public void testDoCancelQueue() throws Exception{ HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false); j.jenkins.setSecurityRealm(realm); User user = realm.createAccount("John Smith", "password"); - SecurityContextHolder.getContext().setAuthentication(user.impersonate()); - try{ + try (ACLContext as = ACL.as(user)) { project.doCancelQueue(null, null); fail("User should not have permission to build project"); } @@ -558,8 +558,7 @@ public void testDoDoDelete() throws Exception{ HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false); j.jenkins.setSecurityRealm(realm); User user = realm.createAccount("John Smith", "password"); - SecurityContextHolder.getContext().setAuthentication(user.impersonate()); - try{ + try (ACLContext as = ACL.as(user)) { project.doDoDelete(null, null); fail("User should not have permission to build project"); } @@ -590,8 +589,7 @@ public void testDoDoWipeOutWorkspace() throws Exception{ HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false); j.jenkins.setSecurityRealm(realm); User user = realm.createAccount("John Smith", "password"); - SecurityContextHolder.getContext().setAuthentication(user.impersonate()); - try{ + try (ACLContext as = ACL.as(user)) { project.doDoWipeOutWorkspace(); fail("User should not have permission to build project"); } @@ -624,8 +622,7 @@ public void testDoDisable() throws Exception{ HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false); j.jenkins.setSecurityRealm(realm); User user = realm.createAccount("John Smith", "password"); - SecurityContextHolder.getContext().setAuthentication(user.impersonate()); - try{ + try (ACLContext as = ACL.as(user)) { project.doDisable(); fail("User should not have permission to build project"); } @@ -655,9 +652,10 @@ public void testDoEnable() throws Exception{ HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false); j.jenkins.setSecurityRealm(realm); User user = realm.createAccount("John Smith", "password"); - SecurityContextHolder.getContext().setAuthentication(user.impersonate()); - project.disable(); - try{ + try (ACLContext as = ACL.as(user)) { + project.disable(); + } + try (ACLContext as = ACL.as(user)) { project.doEnable(); fail("User should not have permission to build project"); }
test/src/test/java/hudson/slaves/CommandLauncher2Test.java+140 −0 added@@ -0,0 +1,140 @@ +/* + * The MIT License + * + * Copyright 2017 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.slaves; + +import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import com.gargoylesoftware.htmlunit.HttpMethod; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlTextInput; +import hudson.cli.CLICommand; +import hudson.cli.CLICommandInvoker; +import hudson.cli.UpdateNodeCommand; +import hudson.model.Computer; +import hudson.model.User; +import java.net.HttpURLConnection; +import jenkins.model.Jenkins; +import org.apache.tools.ant.filters.StringInputStream; +import static org.hamcrest.Matchers.containsString; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.runners.model.Statement; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +public class CommandLauncher2Test { + + @Rule + public RestartableJenkinsRule rr = new RestartableJenkinsRule(); + + @Issue("SECURITY-478") + @Test + public void requireRunScripts() throws Exception { + rr.addStep(new Statement() { + @Override + public void evaluate() throws Throwable { + rr.j.jenkins.setSecurityRealm(rr.j.createDummySecurityRealm()); + rr.j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy(). + grant(Jenkins.ADMINISTER).everywhere().to("admin"). + grant(Jenkins.READ, Computer.CONFIGURE).everywhere().to("dev")); + DumbSlave s = new DumbSlave("s", "/", new CommandLauncher("echo unconfigured")); + rr.j.jenkins.addNode(s); + // First, reconfigure using GUI. + JenkinsRule.WebClient wc = rr.j.createWebClient().login("admin"); + HtmlForm form = wc.getPage(s, "configure").getFormByName("config"); + HtmlTextInput input = form.getInputByName("_.command"); + assertEquals("echo unconfigured", input.getText()); + input.setText("echo configured by GUI"); + rr.j.submit(form); + s = (DumbSlave) rr.j.jenkins.getNode("s"); + assertEquals("echo configured by GUI", ((CommandLauncher) s.getLauncher()).getCommand()); + // Then by REST. + String configDotXml = s.toComputer().getUrl() + "config.xml"; + String xml = wc.goTo(configDotXml, "application/xml").getWebResponse().getContentAsString(); + assertThat(xml, containsString("echo configured by GUI")); + WebRequest req = new WebRequest(wc.createCrumbedUrl(configDotXml), HttpMethod.POST); + req.setEncodingType(null); + req.setRequestBody(xml.replace("echo configured by GUI", "echo configured by REST")); + wc.getPage(req); + s = (DumbSlave) rr.j.jenkins.getNode("s"); + assertEquals("echo configured by REST", ((CommandLauncher) s.getLauncher()).getCommand()); + // Then by CLI. + CLICommand cmd = new UpdateNodeCommand(); + cmd.setTransportAuth(User.get("admin").impersonate()); + assertThat(new CLICommandInvoker(rr.j, cmd).withStdin(new StringInputStream(xml.replace("echo configured by GUI", "echo configured by CLI"))).invokeWithArgs("s"), CLICommandInvoker.Matcher.succeededSilently()); + s = (DumbSlave) rr.j.jenkins.getNode("s"); + assertEquals("echo configured by CLI", ((CommandLauncher) s.getLauncher()).getCommand()); + // Now verify that all modes failed as dev. First as GUI. + s.setLauncher(new CommandLauncher("echo configured by admin")); + wc = rr.j.createWebClient().login("dev"); + form = wc.getPage(s, "configure").getFormByName("config"); + input = form.getInputByName("_.command"); + assertEquals("echo configured by admin", input.getText()); + input.setText("echo ATTACK"); + try { + rr.j.submit(form); + fail(); + } catch (FailingHttpStatusCodeException x) { + assertEquals("403 would be more natural but Descriptor.newInstance wraps AccessDeniedException2 in Error", 500, x.getStatusCode()); + } + s = (DumbSlave) rr.j.jenkins.getNode("s"); + assertEquals("echo configured by admin", ((CommandLauncher) s.getLauncher()).getCommand()); + // Then by REST. + req = new WebRequest(wc.createCrumbedUrl(configDotXml), HttpMethod.POST); + req.setEncodingType(null); + req.setRequestBody(xml.replace("echo configured by GUI", "echo ATTACK")); + try { + wc.getPage(req); + } catch (FailingHttpStatusCodeException x) { + assertEquals(HttpURLConnection.HTTP_FORBIDDEN, x.getStatusCode()); + } + s = (DumbSlave) rr.j.jenkins.getNode("s"); + assertNotEquals(CommandLauncher.class, s.getLauncher().getClass()); // currently seems to reset it to JNLPLauncher, whatever + s.setLauncher(new CommandLauncher("echo configured by admin")); + // Then by CLI. + cmd = new UpdateNodeCommand(); + cmd.setTransportAuth(User.get("dev").impersonate()); + assertThat(new CLICommandInvoker(rr.j, cmd).withStdin(new StringInputStream(xml.replace("echo configured by GUI", "echo ATTACK"))).invokeWithArgs("s"), + CLICommandInvoker.Matcher./* gets swallowed by RobustReflectionConverter, hmm*/succeededSilently()); + s = (DumbSlave) rr.j.jenkins.getNode("s"); + assertNotEquals(CommandLauncher.class, s.getLauncher().getClass()); + // Now also check that SYSTEM deserialization works after a restart. + s.setLauncher(new CommandLauncher("echo configured by admin")); + s.save(); + } + }); + rr.addStep(new Statement() { + @Override + public void evaluate() throws Throwable { + DumbSlave s = (DumbSlave) rr.j.jenkins.getNode("s"); + assertEquals("echo configured by admin", ((CommandLauncher) s.getLauncher()).getCommand()); + } + }); + } + +}
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-j472-mcq2-95p6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2017-1000393ghsaADVISORY
- github.com/jenkinsci/jenkins/commit/67f68c181033cbabf2075769e0f846f58c226c08ghsaWEB
- github.com/jenkinsci/jenkins/commit/d7ea3f40efedd50541a57b943d5f7bbed046d091ghsaWEB
- jenkins.io/security/advisory/2017-10-11ghsaWEB
- jenkins.io/security/advisory/2017-10-11/mitrex_refsource_CONFIRM
News mentions
0No linked articles in our index yet.