CVE-2025-31721
Description
A missing permission check in Jenkins 2.503 and earlier, LTS 2.492.2 and earlier allows attackers with Computer/Create permission but without Computer/Configure permission to copy an agent, gaining access to encrypted secrets in its configuration.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A missing permission check in Jenkins allows attackers with Computer/Create permission to copy an agent and access encrypted secrets in its configuration.
Vulnerability
Overview
CVE-2025-31721 is a missing permission check in Jenkins core versions 2.503 and earlier, and LTS 2.492.2 and earlier. The vulnerability exists in an HTTP endpoint that handles agent copying; it fails to verify that the user has the Computer/Configure permission before allowing the operation [1][2].
Exploitation
An attacker who already has Computer/Create permission (but not Computer/Configure) can exploit this flaw by copying an existing agent. The copy operation exposes the agent's configuration, which may contain encrypted secrets such as credentials or other sensitive data [1][2]. No additional authentication or network position is required beyond having the Computer/Create permission.
Impact
Successful exploitation allows the attacker to retrieve encrypted secrets from the agent configuration. While the secrets are encrypted, their exposure could lead to further compromise if the attacker can decrypt them or use them in other contexts [1].
Mitigation
Jenkins has addressed this issue in versions 2.504 and LTS 2.492.3 by adding a permission check that now requires Computer/Configure permission to copy an agent containing secrets [1][3]. Users are strongly advised to upgrade to these patched versions or apply the relevant security patch.
AI Insight generated on May 20, 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.500, < 2.504 | 2.504 |
org.jenkins-ci.main:jenkins-coreMaven | < 2.492.3 | 2.492.3 |
Affected products
11- Range: <=2.503, LTS <=2.492.2
- osv-coords9 versionspkg:apk/chainguard/jenkins-2.479pkg:apk/chainguard/jenkins-2.479-compatpkg:apk/chainguard/jenkins-2.479-remotingpkg:apk/chainguard/jenkins-2.492pkg:apk/wolfi/jenkins-2.479pkg:apk/wolfi/jenkins-2.479-compatpkg:apk/wolfi/jenkins-2.479-remotingpkg:bitnami/jenkinspkg:maven/org.jenkins-ci.main/jenkins-core
< 0+ 8 more
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 2.492.3-r4
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 2.492.3
- (no CPE)range: >= 2.500, < 2.504
- Jenkins Project/Jenkinsv5Range: 2.492.3
Patches
111 files changed · +99 −0
core/src/main/java/hudson/model/ComputerSet.java+15 −0 modified@@ -61,6 +61,7 @@ import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithChildren; import jenkins.model.ModelObjectWithContextMenu.ContextMenu; +import jenkins.security.ExtendedReadRedaction; import jenkins.util.Timer; import jenkins.widgets.HasWidgets; import net.sf.json.JSONObject; @@ -75,6 +76,7 @@ import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.verb.POST; +import org.springframework.security.access.AccessDeniedException; /** * Serves as the top of {@link Computer}s in the URL hierarchy. @@ -295,6 +297,19 @@ public synchronized void doCreateItem(StaplerRequest2 req, StaplerResponse2 rsp, // copy through XStream String xml = Jenkins.XSTREAM.toXML(src); + if (!src.hasPermission(Computer.CONFIGURE)) { + final String redactedConfigDotXml = ExtendedReadRedaction.applyAll(xml); + if (!xml.equals(redactedConfigDotXml)) { + // AccessDeniedException2 does not permit a custom message, and anyway redirecting the user to the login screen is obviously pointless. + throw new AccessDeniedException( + Messages.ComputerSet_may_not_copy_as_it_contains_secrets_and_( + src.getNodeName(), + Jenkins.getAuthentication2().getName(), + Computer.PERMISSIONS.title, + Computer.EXTENDED_READ.name, + Computer.CONFIGURE.name)); + } + } Node result = (Node) Jenkins.XSTREAM.fromXML(xml); result.setNodeName(name); result.holdOffLaunchUntilSave = true;
core/src/main/resources/hudson/model/Messages_bg.properties+5 −0 modified@@ -155,6 +155,11 @@ CLI.wait-node-offline.shortDescription=\ ComputerSet.DisplayName=\ Компютри +# May not copy {0} as it contains secrets and {1} has {2}/{3} but not /{4} +ComputerSet.may_not_copy_as_it_contains_secrets_and_=\ + Не може да копирате „{0}“, защото на него има пароли, а „{1}“ има {2}/{3}, но\ + не и /{4} + Descriptor.From=\ (от <a href="{1}">{0}</a>)
core/src/main/resources/hudson/model/Messages_de.properties+1 −0 modified@@ -107,6 +107,7 @@ ComputerSet.DisplayName=Knoten ComputerSet.NoSuchSlave=Agent existiert nicht: „{0}“ ComputerSet.SlaveAlreadyExists=Ein Agent mit dem Namen „{0}“ existiert bereits. ComputerSet.SpecifySlaveToCopy=Geben Sie an, welcher Agent kopiert werden soll +ComputerSet.may_not_copy_as_it_contains_secrets_and_=Kann „{0}“ nicht kopieren, da es Geheimnisse enthält und „{1}“ nur {2}/{3} aber nicht /{4} hat. Descriptor.From=(aus <a href="{1}">{0}</a>)
core/src/main/resources/hudson/model/Messages_fr.properties+1 −0 modified@@ -107,6 +107,7 @@ ComputerSet.NoSuchSlave=Aucun agent: {0} ComputerSet.SlaveAlreadyExists=L''agent ‘{0}’ existe déjà ComputerSet.SpecifySlaveToCopy=Quel agent à copier ComputerSet.DisplayName=Nœuds +ComputerSet.may_not_copy_as_it_contains_secrets_and_=Copie impossible de {0} car il contient des secrets et {1} possède {2}/{3} mais pas /{4} Descriptor.From=(de <a href="{1}" rel="noopener noreferrer" target="_blank">{0}</a>)
core/src/main/resources/hudson/model/Messages_it.properties+2 −0 modified@@ -156,6 +156,8 @@ ComputerSet.DisplayName=Nodi ComputerSet.NoSuchSlave=L''agente {0} non esiste ComputerSet.SlaveAlreadyExists=Un agente di nome "{0}" esiste già ComputerSet.SpecifySlaveToCopy=Specificare l''agente da copiare +ComputerSet.may_not_copy_as_it_contains_secrets_and_=Impossibile copiare \ + {0} perché contiene segreti e {1} ha {2}/{3} ma non /{4} Descriptor.From=(da <a href="{1}">{0}</a>) Executor.NotAvailable=N/D FileParameterDefinition.DisplayName=Parametro file
core/src/main/resources/hudson/model/Messages.properties+1 −0 modified@@ -112,6 +112,7 @@ ComputerSet.NoSuchSlave=No such agent: {0} ComputerSet.SlaveAlreadyExists=Agent called ‘{0}’ already exists ComputerSet.SpecifySlaveToCopy=Specify which agent to copy ComputerSet.DisplayName=Nodes +ComputerSet.may_not_copy_as_it_contains_secrets_and_=May not copy {0} as it contains secrets and {1} has {2}/{3} but not /{4} Descriptor.From=(from <a href="{1}" rel="noopener noreferrer" target="_blank">{0}</a>)
core/src/main/resources/hudson/model/Messages_pt_BR.properties+2 −0 modified@@ -105,6 +105,8 @@ UpdateCenter.Status.Success=Sucesso UpdateCenter.init=Iniciando o centro de atualização ParameterAction.DisplayName=Parâmetros ComputerSet.DisplayName=Nós +ComputerSet.may_not_copy_as_it_contains_secrets_and_=Pode não conseguir copiar {0} já que contem segredos e {1} \ + tem {2}/{3} mas não /{4} Run.DeletePermission.Description=\ Esta permissão concede aos usuários o direito de manualmente apagarem construções do histórico de construções. Run.Summary.BrokenForALongTime=Quebrado há muito tempo
core/src/main/resources/hudson/model/Messages_sr.properties+1 −0 modified@@ -85,6 +85,7 @@ Computer.BadChannel=Машина агента није повезана или ComputerSet.SlaveAlreadyExists=Већ постоји агент са именом {0} ComputerSet.SpecifySlaveToCopy=Одредите агент за копирање ComputerSet.DisplayName=Рачунари +ComputerSet.may_not_copy_as_it_contains_secrets_and_=Неможе се копирати {0} пошто садржи тајне и {1} има {2}/{3} а не /{4} Descriptor.From=(од <a href="{1}">{0}</a>) Executor.NotAvailable=Н/Д FreeStyleProject.DisplayName=Независни задатак
core/src/main/resources/hudson/model/Messages_sv_SE.properties+1 −0 modified@@ -112,6 +112,7 @@ ComputerSet.NoSuchSlave=Det finns ingen sådan agent: {0} ComputerSet.SlaveAlreadyExists=En agent med namnet "{0}" finns redan ComputerSet.SpecifySlaveToCopy=Ange vilken agent som ska kopieras ComputerSet.DisplayName=Noder +ComputerSet.may_not_copy_as_it_contains_secrets_and_=Kan inte kopiera {0} eftersom den innehåller hemligheter och {1} har {2}/{3} men inte /{4} Descriptor.From=(från <a href="{1}" rel="noopener noreferrer" target="_blank">{0}</a>)
test/src/test/java/lib/form/PasswordTest.java+64 −0 modified@@ -37,6 +37,7 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.cli.CopyJobCommand; @@ -62,6 +63,7 @@ import hudson.security.ACL; import hudson.slaves.DumbSlave; import hudson.slaves.NodeProperty; +import hudson.slaves.NodePropertyDescriptor; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import hudson.util.FormValidation; @@ -70,6 +72,7 @@ import java.io.File; import java.io.IOException; import java.io.PrintStream; +import java.net.URL; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -82,7 +85,10 @@ import jenkins.security.ExtendedReadRedaction; import jenkins.security.ExtendedReadSecretRedaction; import jenkins.tasks.SimpleBuildStep; +import org.htmlunit.HttpMethod; import org.htmlunit.Page; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; import org.htmlunit.html.DomElement; import org.htmlunit.html.HtmlHiddenInput; import org.htmlunit.html.HtmlInput; @@ -195,16 +201,74 @@ public void testNodeSecrets() throws Exception { } } + @For({ExtendedReadRedaction.class, ExtendedReadSecretRedaction.class}) + @Issue("SECURITY-3513") + @Test + public void testCopyNodeSecrets() throws Exception { + Computer.EXTENDED_READ.setEnabled(true); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + MockAuthorizationStrategy mockAuthorizationStrategy = new MockAuthorizationStrategy(); + mockAuthorizationStrategy.grant(Jenkins.READ, Computer.CREATE, Computer.CONFIGURE).everywhere().to("alice"); + mockAuthorizationStrategy.grant(Jenkins.READ, Computer.CREATE, Computer.EXTENDED_READ).everywhere().to("bob"); + j.jenkins.setAuthorizationStrategy(mockAuthorizationStrategy); + + final DumbSlave onlineSlave = j.createOnlineSlave(); + final String secretText = "t0ps3cr3td4t4_node"; + final Secret encryptedSecret = Secret.fromString(secretText); + final String encryptedSecretText = encryptedSecret.getEncryptedValue(); + + onlineSlave.getNodeProperties().add(new NodePropertyWithSecret(encryptedSecret)); + onlineSlave.save(); + + assertThat(readString(new File(onlineSlave.getRootDir(), "config.xml").toPath()), containsString(encryptedSecretText)); + assertEquals(2, j.getInstance().getComputers().length); + + String agentCopyURL = j.getURL() + "/computer/createItem?mode=copy&from=" + onlineSlave.getNodeName() + "&name="; + + { // with configure, you can copy a node containing secrets + try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) { + WebResponse rsp = wc.getPage(wc.addCrumb(new WebRequest(new URL(agentCopyURL + "aliceAgent"), + HttpMethod.POST))).getWebResponse(); + assertEquals(200, rsp.getStatusCode()); + assertEquals(3, j.getInstance().getComputers().length); + + final Page page = wc.goTo("computer/aliceAgent/config.xml", "application/xml"); + final String content = page.getWebResponse().getContentAsString(); + + assertThat(content, not(containsString(secretText))); + assertThat(content, containsString(encryptedSecretText)); + assertThat(content, containsString("<secret>" + encryptedSecretText + "</secret>")); + } + } + + { // without configure, you cannot copy a node containing secrets + try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false).login("bob")) { + WebResponse rsp = wc.getPage(wc.addCrumb(new WebRequest(new URL(agentCopyURL + "bobAgent"), + HttpMethod.POST))).getWebResponse(); + + assertEquals(403, rsp.getStatusCode()); + assertThat(rsp.getContentAsString(), containsString("May not copy " + onlineSlave.getNodeName() + " as it contains secrets")); + assertEquals(3, j.getInstance().getComputers().length); + } + } + } + public static class NodePropertyWithSecret extends NodeProperty<Node> { private final Secret secret; + @DataBoundConstructor public NodePropertyWithSecret(Secret secret) { this.secret = secret; } public Secret getSecret() { return secret; } + + @Extension + public static class DescriptorImpl extends NodePropertyDescriptor { + + } } @For({ExtendedReadRedaction.class, ExtendedReadSecretRedaction.class})
test/src/test/resources/lib/form/PasswordTest/NodePropertyWithSecret/config.jelly+6 −0 added@@ -0,0 +1,6 @@ +<?jelly escape-by-default='true'?> +<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form"> + <f:entry title="Secret" field="secret"> + <f:password/> + </f:entry> +</j:jelly>
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-wr6w-jxg7-qpfhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-31721ghsaADVISORY
- www.jenkins.io/security/advisory/2025-04-02/ghsavendor-advisoryWEB
- github.com/jenkinsci/jenkins/commit/b3651b475302e8dba20fc63c1ff89d144ec652f0ghsaWEB
News mentions
1- Jenkins Security Advisory 2025-04-02Jenkins Security Advisories · Apr 2, 2025