Jenkins does not encrypt secrets from POST config.xml submissions before storing them in job configurations
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
0 → fixed in 2.555.32.556 → fixed in 2.568
Details
Jenkins 2.567 and earlier, LTS 2.555.2 and earlier does not encrypt secrets from POST config.xml submissions before storing them in job configurations unencrypted in job config.xml files on the Jenkins controller where they can be viewed by users with Item/Extended Read permission, or access to the Jenkins controller file system.
The fix
[SECURITY-3744]
core/src/main/java/hudson/model/AbstractItem.java+29 −30
@@ -43,18 +43,19 @@import hudson.security.AccessControlled;import hudson.util.AlternativeUiTextProvider;import hudson.util.AlternativeUiTextProvider.Message;-import hudson.util.AtomicFileWriter;import hudson.util.FormValidation;-import hudson.util.IOUtils;import hudson.util.Secret;+import hudson.util.XStream2;import hudson.widgets.Widget;import io.jenkins.servlet.ServletExceptionWrapper;import jakarta.servlet.ServletException;+import java.io.ByteArrayOutputStream;import java.io.File;import java.io.IOException;import java.io.OutputStream;-import java.nio.charset.Charset;-import java.nio.file.Files;+import java.io.StringReader;+import java.io.StringWriter;+import java.nio.charset.StandardCharsets;import java.util.Collection;import java.util.List;import java.util.ListIterator;@@ -888,10 +889,13 @@ public void writeConfigDotXml(OutputStream os) throws IOException {checkPermission(EXTENDED_READ);XmlFile configFile = getConfigFile();if (hasPermission(CONFIGURE)) {-IOUtils.copy(configFile.getFile(), os);+Items.XSTREAM2.toXMLUTF8(this, os);} else {+var baos = new ByteArrayOutputStream();+Items.XSTREAM2.toXMLUTF8(this, baos);+String xml = baos.toString(StandardCharsets.UTF_8);+String encoding = configFile.sniffEncoding();-String xml = Files.readString(Util.fileToPath(configFile.getFile()), Charset.forName(encoding));for (ExtendedReadRedaction redaction : ExtendedReadRedaction.all()) {LOGGER.log(Level.FINE, () -> "Applying redaction " + redaction.getClass().getName());@@ -922,34 +926,29 @@ public void updateByXml(StreamSource source) throws IOException {public void updateByXml(Source source) throws IOException {checkPermission(CONFIGURE);XmlFile configXmlFile = getConfigFile();-final AtomicFileWriter out = new AtomicFileWriter(configXmlFile.getFile());+final StringWriter out = new StringWriter();try {-try {-XMLUtils.safeTransform(source, new StreamResult(out));-out.close();-} catch (TransformerException | SAXException e) {-throw new IOException("Failed to persist config.xml", e);-}--// try to reflect the changes by reloading-Object o = new XmlFile(Items.XSTREAM, out.getTemporaryPath().toFile()).unmarshalNullingOut(this);-if (o != this) {-// ensure that we've got the same job type. extending this code to support updating-// to different job type requires destroying & creating a new job type-throw new IOException("Expecting " + this.getClass() + " but got " + o.getClass() + " instead");-}+XMLUtils.safeTransform(source, new StreamResult(out));+out.close();+} catch (TransformerException | SAXException e) {+throw new IOException("Failed to process config.xml", e);+}-Items.runWhileUpdatingByXml(() -> onLoad(getParent(), getRootDir().getName()));-Jenkins.get().rebuildDependencyGraphAsync();+// try to reflect the changes by reloading+Object o = Items.XSTREAM2.unmarshal(XStream2.getDefaultDriver().createReader(new StringReader(out.getBuffer().toString())), this, null, true);+if (o != this) {+// ensure that we've got the same job type. extending this code to support updating+// to different job type requires destroying & creating a new job type+throw new IOException("Expecting " + this.getClass() + " but got " + o.getClass() + " instead");+}-// if everything went well, commit this new version-out.commit();-SaveableListener.fireOnChange(this, getConfigFile());-ItemListener.fireOnUpdated(this);+Items.runWhileUpdatingByXml(() -> onLoad(getParent(), getRootDir().getName()));+Jenkins.get().rebuildDependencyGraphAsync();-} finally {-out.abort(); // don't leave anything behind-}+// if everything went well, re-serialize from memory to encrypt secrets submitted in plaintext+configXmlFile.write(this);+SaveableListener.fireOnChange(this, getConfigFile());+ItemListener.fireOnUpdated(this);}/**
test/src/test/java/hudson/cli/GetJobCommandTest.java+3 −2
@@ -56,12 +56,13 @@ void withFolders() throws Exception {MockFolder d = j.createFolder("d");FreeStyleProject p = d.createProject(FreeStyleProject.class, "p");CLICommandInvoker.Result result = command.invokeWithArgs("d/p");-assertThat(result.stdout(), equalTo(p.getConfigFile().asString()));+// TODO Change XStream2#toXMLUTF8 to use single quotes consistent with XmlFile+assertThat(result.stdout().replace('"', '\''), equalTo(p.getConfigFile().asString().replace('"', '\'')));assertThat(result, hasNoErrorOutput());assertThat(result, succeeded());result = command.invokeWithArgs("d");-assertThat(result.stdout(), equalTo(d.getConfigFile().asString()));+assertThat(result.stdout().replace('"', '\''), equalTo(d.getConfigFile().asString().replace('"', '\'')));assertThat(result, hasNoErrorOutput());assertThat(result, succeeded());}
test/src/test/java/hudson/model/BuildAuthorizationTokenMigrationTest.java+7 −24
@@ -1,6 +1,7 @@package hudson.model;import static org.hamcrest.MatcherAssert.assertThat;+import static org.hamcrest.Matchers.allOf;import static org.hamcrest.Matchers.containsString;import static org.hamcrest.Matchers.equalTo;import static org.hamcrest.Matchers.not;@@ -68,10 +69,10 @@ void basicMigration() throws Throwable {assertThat(authToken.getToken(), equalTo(OLDTOKEN));assertThat(authToken.getEncryptedToken().getPlainText(), equalTo(OLDTOKEN));-// Not redacted yet+// Gets redacted even though it isn't on disktry (JenkinsRule.WebClient wc = j.createWebClient().login(EXTENDED_READER_USERNAME)) {final XmlPage xmlPage = wc.goToXml(fs.getUrl() + "config.xml");-assertThat(xmlPage.getWebResponse().getContentAsString(), containsString("<authToken>" + OLDTOKEN + "</authToken>"));+assertThat(xmlPage.getWebResponse().getContentAsString(), not(containsString("<authToken>" + OLDTOKEN + "</authToken>")));}}@@ -121,7 +122,7 @@ void basicMigration() throws Throwable {}@Test-void testPostConfigXmlAndClearing() throws Exception {+void testPostConfigXmlDoesNotTriggerMonitor() throws Exception {j.jenkins.setSecurityRealm(j.createDummySecurityRealm());j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.READ).everywhere().toEveryone().grant(Jenkins.ADMINISTER).everywhere().to(ADMIN_USERNAME));@@ -140,27 +141,9 @@ void testPostConfigXmlAndClearing() throws Exception {final Page apiPage = wc.getPage(webRequest);assertThat(apiPage.getWebResponse().getStatusCode(), equalTo(200));}--assertThat(j.jenkins.getAdministrativeMonitor("OldData").isActivated(), equalTo(true));--{-// Testing that GET config.xml does not clear the in-memory flag via ConverterImpl-final XmlPage xmlPage = wc.goToXml(freeStyleProject.getUrl() + "config.xml");-assertThat(xmlPage.getWebResponse().getContentAsString(), containsString("plain_token"));--assertThat(freeStyleProject.getConfigFile().asString(), containsString("plain_token"));-}--// Saving clears the admin monitor-freeStyleProject.save();-assertThat(j.jenkins.getAdministrativeMonitor("OldData").isActivated(), equalTo(false));--{-final XmlPage xmlPage = wc.goToXml(freeStyleProject.getUrl() + "config.xml");-assertThat(xmlPage.getWebResponse().getContentAsString(), not(containsString("plain_token")));-assertThat(freeStyleProject.getConfigFile().asString(), not(containsString("plain_token")));-}-}+assertThat(j.jenkins.getAdministrativeMonitor("OldData").isActivated(), equalTo(false));+assertThat(freeStyleProject.getConfigFile().asString(), not(containsString("<authToken>plain_token</authToken>")));+assertThat(freeStyleProject.getConfigFile().asString(), allOf(containsString("<authToken>{"), containsString("}</authToken>")));}}
test/src/test/java/hudson/util/RobustReflectionConverterTest.java+4 −5
@@ -234,12 +234,11 @@ void testRestInterfaceFailure() throws Exception {assertNull(p.getProperty(KeywordProperty.class).getNonCriticalField());assertEquals(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, p.getProperty(KeywordProperty.class).getCriticalField().getKeyword());-// but save to the disk.+// Also not saved to disk as we serialize the object after loadr.jenkins.reload();p = r.jenkins.getItemByFullName(p.getFullName(), FreeStyleProject.class);-assertEquals("badvalue", p.getProperty(KeywordProperty.class).getNonCriticalField().getKeyword());-assertEquals(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, p.getProperty(KeywordProperty.class).getCriticalField().getKeyword());+assertNull(p.getProperty(KeywordProperty.class).getNonCriticalField());}// with addCriticalField. This is not accepted.@@ -306,11 +305,11 @@ void testCliFailure() throws Exception {assertNull(p.getProperty(KeywordProperty.class).getNonCriticalField());assertEquals(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, p.getProperty(KeywordProperty.class).getCriticalField().getKeyword());-// but save to the disk.+// Also not saved to disk as we serialize the object after loadr.jenkins.reload();p = r.jenkins.getItemByFullName(p.getFullName(), FreeStyleProject.class);-assertEquals("badvalue", p.getProperty(KeywordProperty.class).getNonCriticalField().getKeyword());+assertNull(p.getProperty(KeywordProperty.class).getNonCriticalField());}// with addCriticalField. This is not accepted.
test/src/test/java/lib/form/PasswordTest.java+120 −0
@@ -68,17 +68,21 @@import hudson.tasks.Builder;import hudson.util.FormValidation;import hudson.util.Secret;+import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.IOException;import java.io.PrintStream;+import java.io.StringReader;import java.net.URL;+import java.nio.charset.StandardCharsets;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 javax.xml.transform.stream.StreamSource;import jenkins.model.GlobalConfiguration;import jenkins.model.Jenkins;import jenkins.model.TransientActionFactory;@@ -348,6 +352,122 @@ public Secret getSecret() {}}+@Issue("SECURITY-3744")+@Test+void testJobGetConfigXmlAfterRawSubmission() throws Exception {+j.jenkins.setSecurityRealm(j.createDummySecurityRealm());+j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("alice").grant(Jenkins.READ, Item.READ, Item.EXTENDED_READ).everywhere().to("bob"));+final FreeStyleProject freeStyleProject = j.createFreeStyleProject();+freeStyleProject.getBuildersList().add(new SecretBuilder(Secret.fromString("t0ps3cr3t")));+String xmlString;+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(freeStyleProject.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t")));+}+// Now write the job config with a raw string secret+xmlString = xmlString.replaceAll("<secret>[{][^}]+[}]</secret>", "<secret>t0ps3cr3t</secret>");+assertThat(xmlString, containsString("<secret>t0ps3cr3t</secret>"));+freeStyleProject.updateByXml(new StreamSource(new StringReader(xmlString)));+assertThat(freeStyleProject.getConfigFile().asString(), not(containsString("t0ps3cr3t")));++// still encrypted through the API+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(freeStyleProject.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t")));+}+try (JenkinsRule.WebClient wc = j.createWebClient().login("bob")) {+final Page page = wc.goTo(freeStyleProject.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t")));+assertThat(xmlString, containsString("<secret>********</secret>"));+}+}++@Test+void testNodeGetConfigXmlAfterRawSubmission() 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 onlineAgent = j.createOnlineSlave();+onlineAgent.getNodeProperties().add(new NodePropertyWithSecret(Secret.fromString("t0ps3cr3t_node")));+onlineAgent.save();++String xmlString;+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(onlineAgent.getComputer().getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_node")));+}+// Now write the node config with a raw string secret+xmlString = xmlString.replaceAll("<secret>[{][^}]+[}]</secret>", "<secret>t0ps3cr3t_node</secret>");+assertThat(xmlString, containsString("<secret>t0ps3cr3t_node</secret>"));+onlineAgent.getComputer().updateByXml(new ByteArrayInputStream(xmlString.getBytes(StandardCharsets.UTF_8)));+assertThat(j.jenkins.getNodesObject().getConfigFile(onlineAgent).asString(), not(containsString("t0ps3cr3t_node")));++// still encrypted through the API+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(onlineAgent.getComputer().getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_node")));+}+try (JenkinsRule.WebClient wc = j.createWebClient().login("bob")) {+final Page page = wc.goTo(onlineAgent.getComputer().getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_node")));+assertThat(xmlString, containsString("<secret>********</secret>"));+}+}++@Test+void testViewGetConfigXmlAfterRawSubmission() 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 ListView view = new ListView("security-3744-view");+view.getProperties().add(new ViewPropertyWithSecret(Secret.fromString("t0ps3cr3t_view")));+j.jenkins.addView(view);++String xmlString;+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(view.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_view")));+}+// Now write the view config with a raw string secret+xmlString = xmlString.replaceAll("<secret>[{][^}]+[}]</secret>", "<secret>t0ps3cr3t_view</secret>");+assertThat(xmlString, containsString("<secret>t0ps3cr3t_view</secret>"));+view.updateByXml(new StreamSource(new StringReader(xmlString)));++// still encrypted through the API+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(view.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_view")));+}+try (JenkinsRule.WebClient wc = j.createWebClient().login("bob")) {+final Page page = wc.goTo(view.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_view")));+assertThat(xmlString, containsString("<secret>********</secret>"));+}+}++public static class SecretBuilder extends Builder {+private final Secret secret;++@SuppressWarnings("checkstyle:redundantmodifier")+public SecretBuilder(Secret secret) {+this.secret = secret;+}++public Secret getSecret() {+return secret;+}+}+@Issue({"SECURITY-266", "SECURITY-304"})@Test@For(ExtendedReadSecretRedaction.class)
[SECURITY-3744]
core/src/main/java/hudson/model/AbstractItem.java+29 −30
@@ -43,17 +43,18 @@import hudson.security.AccessControlled;import hudson.util.AlternativeUiTextProvider;import hudson.util.AlternativeUiTextProvider.Message;-import hudson.util.AtomicFileWriter;import hudson.util.FormValidation;-import hudson.util.IOUtils;import hudson.util.Secret;+import hudson.util.XStream2;import io.jenkins.servlet.ServletExceptionWrapper;import jakarta.servlet.ServletException;+import java.io.ByteArrayOutputStream;import java.io.File;import java.io.IOException;import java.io.OutputStream;-import java.nio.charset.Charset;-import java.nio.file.Files;+import java.io.StringReader;+import java.io.StringWriter;+import java.nio.charset.StandardCharsets;import java.util.Collection;import java.util.List;import java.util.ListIterator;@@ -883,10 +884,13 @@ public void writeConfigDotXml(OutputStream os) throws IOException {checkPermission(EXTENDED_READ);XmlFile configFile = getConfigFile();if (hasPermission(CONFIGURE)) {-IOUtils.copy(configFile.getFile(), os);+Items.XSTREAM2.toXMLUTF8(this, os);} else {+var baos = new ByteArrayOutputStream();+Items.XSTREAM2.toXMLUTF8(this, baos);+String xml = baos.toString(StandardCharsets.UTF_8);+String encoding = configFile.sniffEncoding();-String xml = Files.readString(Util.fileToPath(configFile.getFile()), Charset.forName(encoding));for (ExtendedReadRedaction redaction : ExtendedReadRedaction.all()) {LOGGER.log(Level.FINE, () -> "Applying redaction " + redaction.getClass().getName());@@ -917,34 +921,29 @@ public void updateByXml(StreamSource source) throws IOException {public void updateByXml(Source source) throws IOException {checkPermission(CONFIGURE);XmlFile configXmlFile = getConfigFile();-final AtomicFileWriter out = new AtomicFileWriter(configXmlFile.getFile());+final StringWriter out = new StringWriter();try {-try {-XMLUtils.safeTransform(source, new StreamResult(out));-out.close();-} catch (TransformerException | SAXException e) {-throw new IOException("Failed to persist config.xml", e);-}--// try to reflect the changes by reloading-Object o = new XmlFile(Items.XSTREAM, out.getTemporaryPath().toFile()).unmarshalNullingOut(this);-if (o != this) {-// ensure that we've got the same job type. extending this code to support updating-// to different job type requires destroying & creating a new job type-throw new IOException("Expecting " + this.getClass() + " but got " + o.getClass() + " instead");-}+XMLUtils.safeTransform(source, new StreamResult(out));+out.close();+} catch (TransformerException | SAXException e) {+throw new IOException("Failed to process config.xml", e);+}-Items.runWhileUpdatingByXml(() -> onLoad(getParent(), getRootDir().getName()));-Jenkins.get().rebuildDependencyGraphAsync();+// try to reflect the changes by reloading+Object o = Items.XSTREAM2.unmarshal(XStream2.getDefaultDriver().createReader(new StringReader(out.getBuffer().toString())), this, null, true);+if (o != this) {+// ensure that we've got the same job type. extending this code to support updating+// to different job type requires destroying & creating a new job type+throw new IOException("Expecting " + this.getClass() + " but got " + o.getClass() + " instead");+}-// if everything went well, commit this new version-out.commit();-SaveableListener.fireOnChange(this, getConfigFile());-ItemListener.fireOnUpdated(this);+Items.runWhileUpdatingByXml(() -> onLoad(getParent(), getRootDir().getName()));+Jenkins.get().rebuildDependencyGraphAsync();-} finally {-out.abort(); // don't leave anything behind-}+// if everything went well, re-serialize from memory to encrypt secrets submitted in plaintext+configXmlFile.write(this);+SaveableListener.fireOnChange(this, getConfigFile());+ItemListener.fireOnUpdated(this);}/**
test/src/test/java/hudson/cli/GetJobCommandTest.java+3 −2
@@ -56,12 +56,13 @@ void withFolders() throws Exception {MockFolder d = j.createFolder("d");FreeStyleProject p = d.createProject(FreeStyleProject.class, "p");CLICommandInvoker.Result result = command.invokeWithArgs("d/p");-assertThat(result.stdout(), equalTo(p.getConfigFile().asString()));+// TODO Change XStream2#toXMLUTF8 to use single quotes consistent with XmlFile+assertThat(result.stdout().replace('"', '\''), equalTo(p.getConfigFile().asString().replace('"', '\'')));assertThat(result, hasNoErrorOutput());assertThat(result, succeeded());result = command.invokeWithArgs("d");-assertThat(result.stdout(), equalTo(d.getConfigFile().asString()));+assertThat(result.stdout().replace('"', '\''), equalTo(d.getConfigFile().asString().replace('"', '\'')));assertThat(result, hasNoErrorOutput());assertThat(result, succeeded());}
test/src/test/java/hudson/model/BuildAuthorizationTokenMigrationTest.java+7 −24
@@ -1,6 +1,7 @@package hudson.model;import static org.hamcrest.MatcherAssert.assertThat;+import static org.hamcrest.Matchers.allOf;import static org.hamcrest.Matchers.containsString;import static org.hamcrest.Matchers.equalTo;import static org.hamcrest.Matchers.not;@@ -68,10 +69,10 @@ void basicMigration() throws Throwable {assertThat(authToken.getToken(), equalTo(OLDTOKEN));assertThat(authToken.getEncryptedToken().getPlainText(), equalTo(OLDTOKEN));-// Not redacted yet+// Gets redacted even though it isn't on disktry (JenkinsRule.WebClient wc = j.createWebClient().login(EXTENDED_READER_USERNAME)) {final XmlPage xmlPage = wc.goToXml(fs.getUrl() + "config.xml");-assertThat(xmlPage.getWebResponse().getContentAsString(), containsString("<authToken>" + OLDTOKEN + "</authToken>"));+assertThat(xmlPage.getWebResponse().getContentAsString(), not(containsString("<authToken>" + OLDTOKEN + "</authToken>")));}}@@ -121,7 +122,7 @@ void basicMigration() throws Throwable {}@Test-void testPostConfigXmlAndClearing() throws Exception {+void testPostConfigXmlDoesNotTriggerMonitor() throws Exception {j.jenkins.setSecurityRealm(j.createDummySecurityRealm());j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.READ).everywhere().toEveryone().grant(Jenkins.ADMINISTER).everywhere().to(ADMIN_USERNAME));@@ -140,27 +141,9 @@ void testPostConfigXmlAndClearing() throws Exception {final Page apiPage = wc.getPage(webRequest);assertThat(apiPage.getWebResponse().getStatusCode(), equalTo(200));}--assertThat(j.jenkins.getAdministrativeMonitor("OldData").isActivated(), equalTo(true));--{-// Testing that GET config.xml does not clear the in-memory flag via ConverterImpl-final XmlPage xmlPage = wc.goToXml(freeStyleProject.getUrl() + "config.xml");-assertThat(xmlPage.getWebResponse().getContentAsString(), containsString("plain_token"));--assertThat(freeStyleProject.getConfigFile().asString(), containsString("plain_token"));-}--// Saving clears the admin monitor-freeStyleProject.save();-assertThat(j.jenkins.getAdministrativeMonitor("OldData").isActivated(), equalTo(false));--{-final XmlPage xmlPage = wc.goToXml(freeStyleProject.getUrl() + "config.xml");-assertThat(xmlPage.getWebResponse().getContentAsString(), not(containsString("plain_token")));-assertThat(freeStyleProject.getConfigFile().asString(), not(containsString("plain_token")));-}-}+assertThat(j.jenkins.getAdministrativeMonitor("OldData").isActivated(), equalTo(false));+assertThat(freeStyleProject.getConfigFile().asString(), not(containsString("<authToken>plain_token</authToken>")));+assertThat(freeStyleProject.getConfigFile().asString(), allOf(containsString("<authToken>{"), containsString("}</authToken>")));}}
test/src/test/java/hudson/util/RobustReflectionConverterTest.java+4 −5
@@ -234,12 +234,11 @@ void testRestInterfaceFailure() throws Exception {assertNull(p.getProperty(KeywordProperty.class).getNonCriticalField());assertEquals(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, p.getProperty(KeywordProperty.class).getCriticalField().getKeyword());-// but save to the disk.+// Also not saved to disk as we serialize the object after loadr.jenkins.reload();p = r.jenkins.getItemByFullName(p.getFullName(), FreeStyleProject.class);-assertEquals("badvalue", p.getProperty(KeywordProperty.class).getNonCriticalField().getKeyword());-assertEquals(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, p.getProperty(KeywordProperty.class).getCriticalField().getKeyword());+assertNull(p.getProperty(KeywordProperty.class).getNonCriticalField());}// with addCriticalField. This is not accepted.@@ -306,11 +305,11 @@ void testCliFailure() throws Exception {assertNull(p.getProperty(KeywordProperty.class).getNonCriticalField());assertEquals(AcceptOnlySpecificKeyword.ACCEPT_KEYWORD, p.getProperty(KeywordProperty.class).getCriticalField().getKeyword());-// but save to the disk.+// Also not saved to disk as we serialize the object after loadr.jenkins.reload();p = r.jenkins.getItemByFullName(p.getFullName(), FreeStyleProject.class);-assertEquals("badvalue", p.getProperty(KeywordProperty.class).getNonCriticalField().getKeyword());+assertNull(p.getProperty(KeywordProperty.class).getNonCriticalField());}// with addCriticalField. This is not accepted.
test/src/test/java/lib/form/PasswordTest.java+120 −0
@@ -68,17 +68,21 @@import hudson.tasks.Builder;import hudson.util.FormValidation;import hudson.util.Secret;+import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.IOException;import java.io.PrintStream;+import java.io.StringReader;import java.net.URL;+import java.nio.charset.StandardCharsets;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 javax.xml.transform.stream.StreamSource;import jenkins.model.GlobalConfiguration;import jenkins.model.Jenkins;import jenkins.model.TransientActionFactory;@@ -348,6 +352,122 @@ public Secret getSecret() {}}+@Issue("SECURITY-3744")+@Test+void testJobGetConfigXmlAfterRawSubmission() throws Exception {+j.jenkins.setSecurityRealm(j.createDummySecurityRealm());+j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("alice").grant(Jenkins.READ, Item.READ, Item.EXTENDED_READ).everywhere().to("bob"));+final FreeStyleProject freeStyleProject = j.createFreeStyleProject();+freeStyleProject.getBuildersList().add(new SecretBuilder(Secret.fromString("t0ps3cr3t")));+String xmlString;+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(freeStyleProject.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t")));+}+// Now write the job config with a raw string secret+xmlString = xmlString.replaceAll("<secret>[{][^}]+[}]</secret>", "<secret>t0ps3cr3t</secret>");+assertThat(xmlString, containsString("<secret>t0ps3cr3t</secret>"));+freeStyleProject.updateByXml(new StreamSource(new StringReader(xmlString)));+assertThat(freeStyleProject.getConfigFile().asString(), not(containsString("t0ps3cr3t")));++// still encrypted through the API+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(freeStyleProject.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t")));+}+try (JenkinsRule.WebClient wc = j.createWebClient().login("bob")) {+final Page page = wc.goTo(freeStyleProject.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t")));+assertThat(xmlString, containsString("<secret>********</secret>"));+}+}++@Test+void testNodeGetConfigXmlAfterRawSubmission() 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 onlineAgent = j.createOnlineSlave();+onlineAgent.getNodeProperties().add(new NodePropertyWithSecret(Secret.fromString("t0ps3cr3t_node")));+onlineAgent.save();++String xmlString;+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(onlineAgent.getComputer().getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_node")));+}+// Now write the node config with a raw string secret+xmlString = xmlString.replaceAll("<secret>[{][^}]+[}]</secret>", "<secret>t0ps3cr3t_node</secret>");+assertThat(xmlString, containsString("<secret>t0ps3cr3t_node</secret>"));+onlineAgent.getComputer().updateByXml(new ByteArrayInputStream(xmlString.getBytes(StandardCharsets.UTF_8)));+assertThat(j.jenkins.getNodesObject().getConfigFile(onlineAgent).asString(), not(containsString("t0ps3cr3t_node")));++// still encrypted through the API+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(onlineAgent.getComputer().getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_node")));+}+try (JenkinsRule.WebClient wc = j.createWebClient().login("bob")) {+final Page page = wc.goTo(onlineAgent.getComputer().getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_node")));+assertThat(xmlString, containsString("<secret>********</secret>"));+}+}++@Test+void testViewGetConfigXmlAfterRawSubmission() 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 ListView view = new ListView("security-3744-view");+view.getProperties().add(new ViewPropertyWithSecret(Secret.fromString("t0ps3cr3t_view")));+j.jenkins.addView(view);++String xmlString;+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(view.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_view")));+}+// Now write the view config with a raw string secret+xmlString = xmlString.replaceAll("<secret>[{][^}]+[}]</secret>", "<secret>t0ps3cr3t_view</secret>");+assertThat(xmlString, containsString("<secret>t0ps3cr3t_view</secret>"));+view.updateByXml(new StreamSource(new StringReader(xmlString)));++// still encrypted through the API+try (JenkinsRule.WebClient wc = j.createWebClient().login("alice")) {+final Page page = wc.goTo(view.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_view")));+}+try (JenkinsRule.WebClient wc = j.createWebClient().login("bob")) {+final Page page = wc.goTo(view.getUrl() + "config.xml", "application/xml");+xmlString = page.getWebResponse().getContentAsString();+assertThat(xmlString, not(containsString("t0ps3cr3t_view")));+assertThat(xmlString, containsString("<secret>********</secret>"));+}+}++public static class SecretBuilder extends Builder {+private final Secret secret;++@SuppressWarnings("checkstyle:redundantmodifier")+public SecretBuilder(Secret secret) {+this.secret = secret;+}++public Secret getSecret() {+return secret;+}+}+@Issue({"SECURITY-266", "SECURITY-304"})@Test@For(ExtendedReadSecretRedaction.class)
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2026-53442
- WEBhttps://github.com/jenkinsci/jenkins/commit/037c2c30cd26b926ec9df3d1b60e16b80608edb4
- WEBhttps://github.com/jenkinsci/jenkins/commit/206f0b565f0ce16b5162160ffb96f5dd59002ff7
- PACKAGEhttps://github.com/jenkinsci/jenkins
- WEBhttps://www.jenkins.io/security/advisory/2026-06-10/#SECURITY-3744