Skip to content

Commit

Permalink
[SECURITY-1923]
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Beck <daniel-beck@users.noreply.github.com>
  • Loading branch information
2 people authored and jenkinsci-cert-ci committed Jan 11, 2021
1 parent f5d9842 commit f1056bd
Show file tree
Hide file tree
Showing 5 changed files with 459 additions and 2 deletions.
32 changes: 30 additions & 2 deletions core/src/main/java/hudson/util/RobustReflectionConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@
import java.util.logging.Logger;
import edu.umd.cs.findbugs.annotations.NonNull;
import net.jcip.annotations.GuardedBy;

import hudson.security.ACL;
import jenkins.model.Jenkins;
import jenkins.util.SystemProperties;
import jenkins.util.xstream.CriticalXStreamException;
import org.acegisecurity.Authentication;

/**
* Custom {@link ReflectionConverter} that handle errors more gracefully.
Expand All @@ -73,6 +78,9 @@
@SuppressWarnings({"rawtypes", "unchecked"})
public class RobustReflectionConverter implements Converter {

private static /* non-final for Groovy */ boolean RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = SystemProperties.getBoolean(RobustReflectionConverter.class.getName() + ".recordFailuresForAllAuthentications", false);
private static /* non-final for Groovy */ boolean RECORD_FAILURES_FOR_ADMINS = SystemProperties.getBoolean(RobustReflectionConverter.class.getName() + ".recordFailuresForAdmins", false);

protected final ReflectionProvider reflectionProvider;
protected final Mapper mapper;
protected transient SerializationMembers serializationMethodInvoker;
Expand Down Expand Up @@ -368,8 +376,8 @@ public Object doUnmarshal(final Object result, final HierarchicalStreamReader re
reader.moveUp();
}

// Report any class/field errors in Saveable objects
if (context.get("ReadError") != null && context.get("Saveable") == result) {
// Report any class/field errors in Saveable objects if it happens during loading of existing data from disk
if (shouldReportUnloadableDataForCurrentUser() && context.get("ReadError") != null && context.get("Saveable") == result) {
// Avoid any error in OldDataMonitor to be catastrophic. See JENKINS-62231 and JENKINS-59582
// The root cause is the OldDataMonitor extension is not ready before a plugin triggers an error, for
// example when trying to load a field that was created by a new version and you downgrade to the previous
Expand All @@ -392,6 +400,26 @@ public Object doUnmarshal(final Object result, final HierarchicalStreamReader re
return result;
}

/**
* Returns whether the current user authentication is allowed to have errors loading data reported.
*
* <p>{@link ACL#SYSTEM} always has errors reported.
* If {@link #RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS} is {@code true}, errors are reported for all authentications.
* Otherwise errors are reported for users with {@link Jenkins#ADMINISTER} permission if {@link #RECORD_FAILURES_FOR_ADMINS} is {@code true}.</p>
*
* @return whether the current user authentication is allowed to have errors loading data reported.
*/
private static boolean shouldReportUnloadableDataForCurrentUser() {
if (RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS) {
return true;
}
final Authentication authentication = Jenkins.getAuthentication();
if (authentication.equals(ACL.SYSTEM)) {
return true;
}
return RECORD_FAILURES_FOR_ADMINS && Jenkins.get().hasPermission(Jenkins.ADMINISTER);
}

public static void addErrorInContext(UnmarshallingContext context, Throwable e) {
LOGGER.log(FINE, "Failed to load", e);
ArrayList<Throwable> list = (ArrayList<Throwable>)context.get("ReadError");
Expand Down
81 changes: 81 additions & 0 deletions test/src/test/java/hudson/model/ComputerSEC1923Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package hudson.model;

import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.WebRequest;
import hudson.ExtensionList;
import hudson.diagnosis.OldDataMonitor;
import jenkins.model.Jenkins;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;

import java.util.Map;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

public class ComputerSEC1923Test {

private static final String CONFIGURATOR = "configure_user";

@Rule
public JenkinsRule j = new JenkinsRule();

@Before
public void setupSecurity() {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
MockAuthorizationStrategy mas = new MockAuthorizationStrategy();
mas.grant(Computer.CONFIGURE, Computer.EXTENDED_READ, Jenkins.READ)
.everywhere()
.to(CONFIGURATOR);
j.jenkins.setAuthorizationStrategy(mas);
}

@Issue("SECURITY-1923")
@Test
public void configDotXmlWithValidXmlAndBadField() throws Exception {
Computer computer = j.createSlave().toComputer();

JenkinsRule.WebClient wc = j.createWebClient();
wc.login(CONFIGURATOR);
WebRequest req = new WebRequest(wc.createCrumbedUrl(String.format("%s/config.xml", computer.getUrl())), HttpMethod.POST);
req.setAdditionalHeader("Content-Type", "application/xml");
req.setRequestBody(VALID_XML_BAD_FIELD_USER_XML);

try {
wc.getPage(req);
fail("Should have returned failure.");
} catch (FailingHttpStatusCodeException e) {
// This really shouldn't return 500, but that's what it does now.
assertThat(e.getStatusCode(), equalTo(500));
}

OldDataMonitor odm = ExtensionList.lookupSingleton(OldDataMonitor.class);
Map<Saveable, OldDataMonitor.VersionRange> data = odm.getData();

assertThat(data.size(), equalTo(0));

odm.doDiscard(null, null);

User.AllUsers.scanAll();
boolean createUser = false;
User badUser = User.getById("foo", createUser);

assertNull("Should not have created user.", badUser);
}

private static final String VALID_XML_BAD_FIELD_USER_XML =
"<hudson.model.User>\n" +
" <id>foo</id>\n" +
" <fullName>Foo User</fullName>\n" +
" <badField/>\n" +
"</hudson.model.User>\n";


}
80 changes: 80 additions & 0 deletions test/src/test/java/hudson/model/FreeStyleProjectSEC1923Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package hudson.model;

import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.WebRequest;
import hudson.ExtensionList;
import hudson.diagnosis.OldDataMonitor;
import jenkins.model.Jenkins;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;

import java.util.Map;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

public class FreeStyleProjectSEC1923Test {

private static final String CONFIGURATOR = "configure_user";

@Rule
public JenkinsRule j = new JenkinsRule();

@Before
public void setupSecurity() {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
MockAuthorizationStrategy mas = new MockAuthorizationStrategy();
mas.grant(Item.CONFIGURE, Item.READ, Jenkins.READ)
.everywhere()
.to(CONFIGURATOR);
j.jenkins.setAuthorizationStrategy(mas);
}

@Issue("SECURITY-1923")
@Test
public void configDotXmlWithValidXmlAndBadField() throws Exception {
FreeStyleProject project = j.createFreeStyleProject();

JenkinsRule.WebClient wc = j.createWebClient();
wc.login(CONFIGURATOR);
WebRequest req = new WebRequest(wc.createCrumbedUrl(String.format("%s/config.xml", project.getUrl())), HttpMethod.POST);
req.setAdditionalHeader("Content-Type", "application/xml");
req.setRequestBody(VALID_XML_BAD_FIELD_USER_XML);

try {
wc.getPage(req);
fail("Should have returned failure.");
} catch (FailingHttpStatusCodeException e) {
// This really shouldn't return 500, but that's what it does now.
assertThat(e.getStatusCode(), equalTo(500));
}

OldDataMonitor odm = ExtensionList.lookupSingleton(OldDataMonitor.class);
Map<Saveable, OldDataMonitor.VersionRange> data = odm.getData();

assertThat(data.size(), equalTo(0));

odm.doDiscard(null, null);

User.AllUsers.scanAll();
boolean createUser = false;
User badUser = User.getById("foo", createUser);

assertNull("Should not have created user.", badUser);
}

private static final String VALID_XML_BAD_FIELD_USER_XML =
"<hudson.model.User>\n" +
" <id>foo</id>\n" +
" <fullName>Foo User</fullName>\n" +
" <badField/>\n" +
"</hudson.model.User>\n";

}
81 changes: 81 additions & 0 deletions test/src/test/java/hudson/model/ItemGroupMixInSEC1923Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package hudson.model;


import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.WebRequest;
import hudson.ExtensionList;
import hudson.diagnosis.OldDataMonitor;
import jenkins.model.Jenkins;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;

import java.net.URL;
import java.util.Map;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

public class ItemGroupMixInSEC1923Test {

private static final String CREATOR = "create_user";

@Rule
public JenkinsRule j = new JenkinsRule();

@Before
public void setupSecurity() {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
MockAuthorizationStrategy mas = new MockAuthorizationStrategy();
mas.grant(Item.CREATE, Item.CONFIGURE, Item.READ, Jenkins.READ)
.everywhere()
.to(CREATOR);
j.jenkins.setAuthorizationStrategy(mas);
}

@Issue("SECURITY-1923")
@Test
public void doCreateItemWithValidXmlAndBadField() throws Exception {
JenkinsRule.WebClient wc = j.createWebClient();
wc.login(CREATOR);
WebRequest req = new WebRequest(wc.createCrumbedUrl("createItem?name=testProject"), HttpMethod.POST);
req.setAdditionalHeader("Content-Type", "application/xml");
req.setRequestBody(VALID_XML_BAD_FIELD_USER_XML);

try {
wc.getPage(req);
fail("Should have returned failure.");
} catch (FailingHttpStatusCodeException e) {
// This really shouldn't return 500, but that's what it does now.
assertThat(e.getStatusCode(), equalTo(500));
}

OldDataMonitor odm = ExtensionList.lookupSingleton(OldDataMonitor.class);
Map<Saveable, OldDataMonitor.VersionRange> data = odm.getData();

assertThat(data.size(), equalTo(0));

odm.doDiscard(null, null);

User.AllUsers.scanAll();
boolean createUser = false;
User badUser = User.getById("foo", createUser);

assertNull("Should not have created user.", badUser);
}

private static final String VALID_XML_BAD_FIELD_USER_XML =
"<hudson.model.User>\n" +
" <id>foo</id>\n" +
" <fullName>Foo User</fullName>\n" +
" <badField/>\n" +
"</hudson.model.User>\n";
}
Loading

0 comments on commit f1056bd

Please sign in to comment.