VYPR
Moderate severityNVD Advisory· Published Apr 2, 2025· Updated Apr 2, 2025

CVE-2025-31721

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.

PackageAffected versionsPatched versions
org.jenkins-ci.main:jenkins-coreMaven
>= 2.500, < 2.5042.504
org.jenkins-ci.main:jenkins-coreMaven
< 2.492.32.492.3

Affected products

11

Patches

1
b3651b475302

[SECURITY-3513]

https://github.com/jenkinsci/jenkinsDaniel BeckMar 25, 2025via ghsa
11 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

News mentions

1