Security context
Medium· 4.3GHSA-p34j-r3ch-c985 CVE-2025-27622CWE-312Published Mar 6, 2025

Jenkins reveals encrypted values of secrets stored in agent configuration to users with Agent/Extended Read permission

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

2.493 → fixed in 2.5000 → fixed in 2.492.2

Details

Jenkins 2.499 and earlier, LTS 2.492.1 and earlier does not redact encrypted values of secrets when accessing `config.xml` of agents via REST API or CLI. This allows attackers with Agent/Extended Read permission to view encrypted values of secrets. Jenkins 2.500, LTS 2.492.2 redacts the encrypted values of secrets stored in agent `config.xml` accessed via REST API or CLI for users lacking Agent/Configure permission.

The fix

[SECURITY-3495][SECURITY-3496]

Daniel Beck· Feb 25, 2025, 04:17 PM+2044923cdbc165
core/src/main/java/hudson/cli/GetNodeCommand.java+13 1
@@ -27,8 +27,11 @@
import hudson.Extension;
import hudson.model.Computer;
import hudson.model.Node;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import jenkins.model.Jenkins;
+import jenkins.security.ExtendedReadRedaction;
import org.kohsuke.args4j.Argument;
/**
@@ -52,7 +55,16 @@ protected int run() throws IOException {
node.checkPermission(Computer.EXTENDED_READ);
- Jenkins.XSTREAM2.toXMLUTF8(node, stdout);
+ if (node.hasPermission(Computer.CONFIGURE)) {
+ Jenkins.XSTREAM2.toXMLUTF8(node, stdout);
+ } else {
+ var baos = new ByteArrayOutputStream();
+ Jenkins.XSTREAM2.toXMLUTF8(node, baos);
+ String xml = baos.toString(StandardCharsets.UTF_8);
+
+ xml = ExtendedReadRedaction.applyAll(xml);
+ org.apache.commons.io.IOUtils.write(xml, stdout, StandardCharsets.UTF_8);
+ }
return 0;
}
core/src/main/java/hudson/cli/GetViewCommand.java+14 1
@@ -26,6 +26,9 @@
import hudson.Extension;
import hudson.model.View;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import jenkins.security.ExtendedReadRedaction;
import org.kohsuke.args4j.Argument;
/**
@@ -48,7 +51,17 @@ public String getShortDescription() {
protected int run() throws Exception {
view.checkPermission(View.READ);
- view.writeXml(stdout);
+
+ if (view.hasPermission(View.CONFIGURE)) {
+ view.writeXml(stdout);
+ } else {
+ var baos = new ByteArrayOutputStream();
+ view.writeXml(baos);
+ String xml = baos.toString(StandardCharsets.UTF_8);
+
+ xml = ExtendedReadRedaction.applyAll(xml);
+ org.apache.commons.io.IOUtils.write(xml, stdout, StandardCharsets.UTF_8);
+ }
return 0;
}
core/src/main/java/hudson/model/Computer.java+13 1
@@ -74,6 +74,7 @@
import hudson.util.RemotingDiagnostics.HeapDump;
import hudson.util.RunList;
import jakarta.servlet.ServletException;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -83,6 +84,7 @@
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.StandardCopyOption;
@@ -110,6 +112,7 @@
import jenkins.model.IComputer;
import jenkins.model.IDisplayExecutor;
import jenkins.model.Jenkins;
+import jenkins.security.ExtendedReadRedaction;
import jenkins.security.ImpersonatingExecutorService;
import jenkins.security.MasterToSlaveCallable;
import jenkins.security.stapler.StaplerDispatchable;
@@ -1526,7 +1529,16 @@ public void doConfigDotXml(StaplerRequest2 req, StaplerResponse2 rsp)
if (node == null) {
throw HttpResponses.notFound();
}
- Jenkins.XSTREAM2.toXMLUTF8(node, rsp.getOutputStream());
+ if (hasPermission(CONFIGURE)) {
+ Jenkins.XSTREAM2.toXMLUTF8(node, rsp.getOutputStream());
+ } else {
+ var baos = new ByteArrayOutputStream();
+ Jenkins.XSTREAM2.toXMLUTF8(node, baos);
+ String xml = baos.toString(StandardCharsets.UTF_8);
+
+ xml = ExtendedReadRedaction.applyAll(xml);
+ org.apache.commons.io.IOUtils.write(xml, rsp.getOutputStream(), StandardCharsets.UTF_8);
+ }
return;
}
if (req.getMethod().equals("POST")) {
core/src/main/java/hudson/model/View.java+12 1
@@ -62,6 +62,7 @@
import jakarta.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -94,6 +95,7 @@
import jenkins.model.item_category.Categories;
import jenkins.model.item_category.Category;
import jenkins.model.item_category.ItemCategory;
+import jenkins.security.ExtendedReadRedaction;
import jenkins.security.stapler.StaplerNotDispatchable;
import jenkins.util.xml.XMLUtils;
import jenkins.widgets.HasWidgets;
@@ -983,7 +985,16 @@ private HttpResponse doConfigDotXmlImpl(StaplerRequest2 req) throws IOException
@Override
public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object node) throws IOException, ServletException {
rsp.setContentType("application/xml");
- View.this.writeXml(rsp.getOutputStream());
+ if (hasPermission(CONFIGURE)) {
+ View.this.writeXml(rsp.getOutputStream());
+ } else {
+ var baos = new ByteArrayOutputStream();
+ View.this.writeXml(baos);
+ String xml = baos.toString(StandardCharsets.UTF_8);
+
+ xml = ExtendedReadRedaction.applyAll(xml);
+ org.apache.commons.io.IOUtils.write(xml, rsp.getOutputStream(), StandardCharsets.UTF_8);
+ }
}
};
}
test/src/test/java/lib/form/PasswordTest.java+152 0
@@ -24,6 +24,7 @@
package lib.form;
+import static java.nio.file.Files.readString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
@@ -40,6 +41,8 @@
import hudson.Launcher;
import hudson.cli.CopyJobCommand;
import hudson.cli.GetJobCommand;
+import hudson.cli.GetNodeCommand;
+import hudson.cli.GetViewCommand;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Computer;
@@ -48,25 +51,35 @@
import hudson.model.Job;
import hudson.model.JobProperty;
import hudson.model.JobPropertyDescriptor;
+import hudson.model.ListView;
+import hudson.model.Node;
import hudson.model.RootAction;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.User;
+import hudson.model.View;
+import hudson.model.ViewProperty;
+import hudson.security.ACL;
+import hudson.slaves.DumbSlave;
+import hudson.slaves.NodeProperty;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.util.FormValidation;
import hudson.util.Secret;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.regex.Pattern;
import jenkins.model.GlobalConfiguration;
import jenkins.model.Jenkins;
import jenkins.model.TransientActionFactory;
+import jenkins.security.ExtendedReadRedaction;
import jenkins.security.ExtendedReadSecretRedaction;
import jenkins.tasks.SimpleBuildStep;
import org.htmlunit.Page;
@@ -124,6 +137,145 @@ public String getUrlName() {
}
}
+ @For({ExtendedReadRedaction.class, ExtendedReadSecretRedaction.class})
+ @Issue("SECURITY-3495")
+ @Test
+ public void testNodeSecrets() throws Exception {
+ Computer.EXTENDED_READ.setEnabled(true);
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("alice").grant(Jenkins.READ, Computer.EXTENDED_READ).everywhere().to("bob"));
+
+ 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));
+
+
+ { // admin can see encrypted value
+ GetNodeCommand command = new GetNodeCommand();
+ try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {
+ final Page page = wc.goTo(onlineSlave.getComputer().getUrl() + "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>"));
+
+ var baos = new ByteArrayOutputStream();
+ try (var unused = ACL.as(User.get("alice", true, Map.of()))) {
+ command.setTransportAuth2(Jenkins.getAuthentication2());
+ command.main(List.of(onlineSlave.getNodeName()), Locale.US, System.in, new PrintStream(baos), System.err);
+ }
+ assertEquals(content, baos.toString(page.getWebResponse().getContentCharset()));
+ }
+ }
+
+ { // extended reader gets only redacted value
+ GetNodeCommand command = new GetNodeCommand();
+ try (JenkinsRule.WebClient wc = j.createWebClient().login("bob")) {
+ final Page page = wc.goTo(onlineSlave.getComputer().getUrl() + "config.xml", "application/xml");
+ final String content = page.getWebResponse().getContentAsString();
+
+ assertThat(content, not(containsString(secretText)));
+ assertThat(content, not(containsString(encryptedSecretText)));
+ assertThat(content, containsString("<secret>********</secret>"));
+
+ var baos = new ByteArrayOutputStream();
+ try (var unused = ACL.as(User.get("bob", true, Map.of()))) {
+ command.setTransportAuth2(Jenkins.getAuthentication2());
+ command.main(List.of(onlineSlave.getNodeName()), Locale.US, System.in, new PrintStream(baos), System.err);
+ }
+ assertEquals(content, baos.toString(page.getWebResponse().getContentCharset()));
+ }
+ }
+ }
+
+ public static class NodePropertyWithSecret extends NodeProperty<Node> {
+ private final Secret secret;
+
+ public NodePropertyWithSecret(Secret secret) {
+ this.secret = secret;
+ }
+
+ public Secret getSecret() {
+ return secret;
+ }
+ }
+
+ @For({ExtendedReadRedaction.class, ExtendedReadSecretRedaction.class})
+ @Issue("SECURITY-3496")
+ @Test
+ public void testViewSecrets() throws Exception {
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("alice").grant(Jenkins.READ, View.READ).everywhere().to("bob"));
+
+ final String secretText = "t0ps3cr3td4t4_view";
+ final Secret encryptedSecret = Secret.fromString(secretText);
+ final String encryptedSecretText = encryptedSecret.getEncryptedValue();
+
+ final ListView v = new ListView("security-3496");
+ v.getProperties().add(new ViewPropertyWithSecret(encryptedSecret));
+ j.jenkins.addView(v);
+
+ assertThat(readString(new File(j.jenkins.getRootDir(), "config.xml").toPath()), containsString(encryptedSecretText));
+
+
+ { // admin can see encrypted value
+ var command = new GetViewCommand();
+ try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {
+ final Page page = wc.goTo(v.getUrl() + "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>"));
+
+ var baos = new ByteArrayOutputStream();
+ try (var unused = ACL.as(User.get("alice", true, Map.of()))) {
+ command.setTransportAuth2(Jenkins.getAuthentication2());
+ command.main(List.of(v.getViewName()), Locale.US, System.in, new PrintStream(baos), System.err);
+ }
+ assertEquals(content, baos.toString(page.getWebResponse().getContentCharset()));
+ }
+ }
+
+ { // extended reader gets only redacted value
+ var command = new GetViewCommand();
+ try (JenkinsRule.WebClient wc = j.createWebClient().login("bob")) {
+ final Page page = wc.goTo(v.getUrl() + "config.xml", "application/xml");
+ final String content = page.getWebResponse().getContentAsString();
+
+ assertThat(content, not(containsString(secretText)));
+ assertThat(content, not(containsString(encryptedSecretText)));
+ assertThat(content, containsString("<secret>********</secret>"));
+
+ var baos = new ByteArrayOutputStream();
+ try (var unused = ACL.as(User.get("bob", true, Map.of()))) {
+ command.setTransportAuth2(Jenkins.getAuthentication2());
+ command.main(List.of(v.getViewName()), Locale.US, System.in, new PrintStream(baos), System.err);
+ }
+ assertEquals(content, baos.toString(page.getWebResponse().getContentCharset()));
+ }
+ }
+ }
+
+ public static class ViewPropertyWithSecret extends ViewProperty {
+ private final Secret secret;
+
+ public ViewPropertyWithSecret(Secret secret) {
+ this.secret = secret;
+ }
+
+ public Secret getSecret() {
+ return secret;
+ }
+ }
+
@Issue({"SECURITY-266", "SECURITY-304"})
@Test
@For(ExtendedReadSecretRedaction.class)

References