mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-10-26 19:15:09 +00:00
[SECURITY-3744]
This commit is contained in:
1 parent
c45e93f2d7
commit
037c2c30cd
5 files changed
+165
-63
No files matched your search
@@ -43,18 +43,19 @@ import hudson.security.ACLContext;
|
||||
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 abstract class AbstractItem extends Actionable implements Loadable, Item,
|
||||
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 abstract class AbstractItem extends Actionable implements Loadable, Item,
|
||||
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");
|
||||
}
|
||||
|
||||
Items.runWhileUpdatingByXml(() -> onLoad(getParent(), getRootDir().getName()));
|
||||
Jenkins.get().rebuildDependencyGraphAsync();
|
||||
|
||||
// if everything went well, commit this new version
|
||||
out.commit();
|
||||
SaveableListener.fireOnChange(this, getConfigFile());
|
||||
ItemListener.fireOnUpdated(this);
|
||||
|
||||
} finally {
|
||||
out.abort(); // don't leave anything behind
|
||||
XMLUtils.safeTransform(source, new StreamResult(out));
|
||||
out.close();
|
||||
} catch (TransformerException | SAXException e) {
|
||||
throw new IOException("Failed to process config.xml", e);
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
Items.runWhileUpdatingByXml(() -> onLoad(getParent(), getRootDir().getName()));
|
||||
Jenkins.get().rebuildDependencyGraphAsync();
|
||||
|
||||
// if everything went well, re-serialize from memory to encrypt secrets submitted in plaintext
|
||||
configXmlFile.write(this);
|
||||
SaveableListener.fireOnChange(this, getConfigFile());
|
||||
ItemListener.fireOnUpdated(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,12 +56,13 @@ class GetJobCommandTest {
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -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 @@ public class BuildAuthorizationTokenMigrationTest {
|
||||
assertThat(authToken.getToken(), equalTo(OLDTOKEN));
|
||||
assertThat(authToken.getEncryptedToken().getPlainText(), equalTo(OLDTOKEN));
|
||||
|
||||
// Not redacted yet
|
||||
// Gets redacted even though it isn't on disk
|
||||
try (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 @@ public class BuildAuthorizationTokenMigrationTest {
|
||||
}
|
||||
|
||||
@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 @@ public class BuildAuthorizationTokenMigrationTest {
|
||||
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>")));
|
||||
}
|
||||
}
|
||||
@@ -234,12 +234,11 @@ class RobustReflectionConverterTest {
|
||||
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 load
|
||||
r.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 @@ class RobustReflectionConverterTest {
|
||||
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 load
|
||||
r.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.
|
||||
|
||||
@@ -68,17 +68,21 @@ import hudson.tasks.BuildStepDescriptor;
|
||||
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 @@ class PasswordTest {
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
Reference in new issue
Block a user