mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-11-06 16:45:58 +00:00
Merge branch 'security-stable-2.150' into security-master
This commit is contained in:
commit
db5defdf2f
49 files changed
+5045
-3
No files matched your search
+6
-1
@@ -39,7 +39,7 @@ THE SOFTWARE.
|
||||
|
||||
<properties>
|
||||
<staplerFork>true</staplerFork>
|
||||
<stapler.version>1.255</stapler.version>
|
||||
<stapler.version>1.256</stapler.version>
|
||||
<spring.version>2.5.6.SEC03</spring.version>
|
||||
<groovy.version>2.4.12</groovy.version>
|
||||
</properties>
|
||||
@@ -177,6 +177,11 @@ THE SOFTWARE.
|
||||
<classifier>tests</classifier>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jenkins.stapler</groupId>
|
||||
<artifactId>jenkins-stapler-support</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-library</artifactId>
|
||||
|
||||
@@ -50,6 +50,7 @@ import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.annotation.CheckForNull;
|
||||
import jenkins.model.Jenkins;
|
||||
import jenkins.security.stapler.StaplerAccessibleType;
|
||||
import jenkins.util.JenkinsJVM;
|
||||
import jenkins.util.SystemProperties;
|
||||
import org.apache.commons.httpclient.Credentials;
|
||||
@@ -78,6 +79,7 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
|
||||
*
|
||||
* @see jenkins.model.Jenkins#proxy
|
||||
*/
|
||||
@StaplerAccessibleType
|
||||
public final class ProxyConfiguration extends AbstractDescribableImpl<ProxyConfiguration> implements Saveable, Serializable {
|
||||
/**
|
||||
* Holds a default TCP connect timeout set on all connections returned from this class,
|
||||
|
||||
@@ -34,6 +34,7 @@ import javax.annotation.Nullable;
|
||||
import hudson.model.AperiodicWork;
|
||||
import jenkins.model.Jenkins;
|
||||
import jenkins.model.identity.InstanceIdentityProvider;
|
||||
import jenkins.security.stapler.StaplerAccessibleType;
|
||||
import jenkins.slaves.RemotingVersionInfo;
|
||||
import jenkins.util.SystemProperties;
|
||||
import hudson.slaves.OfflineCause;
|
||||
@@ -82,6 +83,7 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @see AgentProtocol
|
||||
*/
|
||||
@StaplerAccessibleType
|
||||
public final class TcpSlaveAgentListener extends Thread {
|
||||
|
||||
private final ServerSocketChannel serverSocket;
|
||||
|
||||
@@ -26,6 +26,7 @@ package hudson.diagnosis;
|
||||
import hudson.Extension;
|
||||
import hudson.Util;
|
||||
import hudson.model.AdministrativeMonitor;
|
||||
import jenkins.security.stapler.StaplerDispatchable;
|
||||
import org.jenkinsci.Symbol;
|
||||
import org.kohsuke.stapler.HttpRedirect;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
@@ -70,6 +71,7 @@ public class ReverseProxySetupMonitor extends AdministrativeMonitor {
|
||||
return new HttpRedirect(redirect);
|
||||
}
|
||||
|
||||
@StaplerDispatchable
|
||||
public void getTestForReverseProxySetup(String rest) {
|
||||
Jenkins j = Jenkins.getInstance();
|
||||
String inferred = j.getRootUrlFromRequest() + "manage";
|
||||
|
||||
@@ -30,6 +30,8 @@ import hudson.EnvVars;
|
||||
import hudson.Extension;
|
||||
import hudson.Launcher.ProcStarter;
|
||||
import hudson.slaves.Cloud;
|
||||
import jenkins.security.stapler.StaplerDispatchable;
|
||||
import jenkins.util.SystemProperties;
|
||||
import hudson.Util;
|
||||
import hudson.cli.declarative.CLIResolver;
|
||||
import hudson.console.AnnotatedLargeText;
|
||||
@@ -962,6 +964,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
|
||||
* Gets the read-only snapshot view of all {@link Executor}s.
|
||||
*/
|
||||
@Exported
|
||||
@StaplerDispatchable
|
||||
public List<Executor> getExecutors() {
|
||||
return new ArrayList<Executor>(executors);
|
||||
}
|
||||
@@ -970,6 +973,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
|
||||
* Gets the read-only snapshot view of all {@link OneOffExecutor}s.
|
||||
*/
|
||||
@Exported
|
||||
@StaplerDispatchable
|
||||
public List<OneOffExecutor> getOneOffExecutors() {
|
||||
return new ArrayList<OneOffExecutor>(oneOffExecutors);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
*/
|
||||
package hudson.model;
|
||||
|
||||
import jenkins.security.stapler.StaplerAccessibleType;
|
||||
|
||||
/**
|
||||
* A model object has a human readable name.
|
||||
*
|
||||
@@ -32,6 +34,7 @@ package hudson.model;
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
@StaplerAccessibleType
|
||||
public interface ModelObject {
|
||||
String getDisplayName();
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import java.util.logging.Logger;
|
||||
import javax.annotation.CheckForNull;
|
||||
import jenkins.model.Jenkins;
|
||||
|
||||
import jenkins.security.stapler.StaplerAccessibleType;
|
||||
import net.sf.json.JSONObject;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.DoNotUse;
|
||||
@@ -75,6 +76,7 @@ import org.kohsuke.stapler.export.ExportedBean;
|
||||
* @see ParametersAction
|
||||
*/
|
||||
@ExportedBean(defaultVisibility=3)
|
||||
@StaplerAccessibleType
|
||||
public abstract class ParameterValue implements Serializable {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(ParameterValue.class.getName());
|
||||
|
||||
@@ -69,6 +69,7 @@ import java.nio.file.Files;
|
||||
|
||||
import hudson.util.Futures;
|
||||
import jenkins.security.QueueItemAuthenticatorProvider;
|
||||
import jenkins.security.stapler.StaplerAccessibleType;
|
||||
import jenkins.util.SystemProperties;
|
||||
import jenkins.util.Timer;
|
||||
import hudson.triggers.SafeTimerTask;
|
||||
@@ -2019,6 +2020,7 @@ public class Queue extends ResourceController implements Saveable {
|
||||
* Implementation must have {@code executorCell.jelly}, which is
|
||||
* used to render the HTML that indicates this executable is executing.
|
||||
*/
|
||||
@StaplerAccessibleType
|
||||
public interface Executable extends Runnable {
|
||||
/**
|
||||
* Task from which this executable was created.
|
||||
|
||||
@@ -34,6 +34,8 @@ import hudson.ProxyConfiguration;
|
||||
import hudson.security.ACLContext;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
|
||||
import jenkins.security.stapler.StaplerDispatchable;
|
||||
import jenkins.util.SystemProperties;
|
||||
import hudson.Util;
|
||||
import hudson.XmlFile;
|
||||
@@ -318,6 +320,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
|
||||
* can be empty but never null. Oldest entries first.
|
||||
*/
|
||||
@Exported
|
||||
@StaplerDispatchable
|
||||
public List<UpdateCenterJob> getJobs() {
|
||||
synchronized (jobs) {
|
||||
return new ArrayList<UpdateCenterJob>(jobs);
|
||||
@@ -518,6 +521,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
|
||||
* @return
|
||||
* can be empty but never null.
|
||||
*/
|
||||
@StaplerDispatchable // referenced by _api.jelly
|
||||
public PersistedList<UpdateSite> getSites() {
|
||||
return sites;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ import jenkins.model.item_category.Categories;
|
||||
import jenkins.model.item_category.Category;
|
||||
import jenkins.model.item_category.ItemCategory;
|
||||
import jenkins.scm.RunWithSCM;
|
||||
import jenkins.security.stapler.StaplerAccessibleType;
|
||||
import jenkins.util.ProgressiveRendering;
|
||||
import jenkins.util.xml.XMLUtils;
|
||||
|
||||
@@ -697,6 +698,7 @@ public abstract class View extends AbstractModelObject implements AccessControll
|
||||
}
|
||||
|
||||
@ExportedBean
|
||||
@StaplerAccessibleType
|
||||
public static final class People {
|
||||
@Exported
|
||||
public final List<UserInfo> users;
|
||||
|
||||
@@ -36,6 +36,7 @@ import java.util.Collections;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import jenkins.model.Jenkins;
|
||||
import jenkins.security.stapler.StaplerAccessibleType;
|
||||
import net.sf.json.JSONObject;
|
||||
|
||||
import org.acegisecurity.Authentication;
|
||||
@@ -62,6 +63,7 @@ import org.kohsuke.stapler.StaplerRequest;
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @see SecurityRealm
|
||||
*/
|
||||
@StaplerAccessibleType
|
||||
public abstract class AuthorizationStrategy extends AbstractDescribableImpl<AuthorizationStrategy> implements ExtensionPoint {
|
||||
/**
|
||||
* Returns the instance of {@link ACL} where all the other {@link ACL} instances
|
||||
|
||||
@@ -9,6 +9,7 @@ import javax.servlet.ServletRequest;
|
||||
|
||||
import hudson.init.Initializer;
|
||||
import jenkins.model.Jenkins;
|
||||
import jenkins.security.stapler.StaplerAccessibleType;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.kohsuke.stapler.WebApp;
|
||||
@@ -40,6 +41,7 @@ import org.kohsuke.stapler.StaplerResponse;
|
||||
* @see <a href="http://en.wikipedia.org/wiki/XSRF">Wikipedia: Cross site request forgery</a>
|
||||
*/
|
||||
@ExportedBean
|
||||
@StaplerAccessibleType
|
||||
public abstract class CrumbIssuer implements Describable<CrumbIssuer>, ExtensionPoint {
|
||||
|
||||
private static final String CRUMB_ATTRIBUTE = CrumbIssuer.class.getName() + "_crumb";
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.OpenOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import jenkins.model.Jenkins;
|
||||
import jenkins.security.stapler.StaplerDispatchable;
|
||||
import org.apache.tools.ant.DirectoryScanner;
|
||||
import org.apache.tools.ant.Project;
|
||||
import org.apache.tools.ant.types.FileSet;
|
||||
@@ -94,6 +95,7 @@ public class HsErrPidList extends AdministrativeMonitor {
|
||||
/**
|
||||
* Expose files to the URL.
|
||||
*/
|
||||
@StaplerDispatchable
|
||||
public List<HsErrPidFile> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import jenkins.model.Jenkins;
|
||||
import jenkins.model.JenkinsLocationConfiguration;
|
||||
import jenkins.security.stapler.StaplerAccessibleType;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
/**
|
||||
* Jenkins install state.
|
||||
@@ -45,6 +46,7 @@ import org.apache.commons.lang.StringUtils;
|
||||
*
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
@StaplerAccessibleType
|
||||
public class InstallState implements ExtensionPoint {
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,11 @@ import hudson.*;
|
||||
import hudson.Launcher.LocalLauncher;
|
||||
import jenkins.AgentProtocol;
|
||||
import jenkins.diagnostics.URICheckEncodingMonitor;
|
||||
import jenkins.security.stapler.DoActionFilter;
|
||||
import jenkins.security.stapler.StaplerFilteredActionListener;
|
||||
import jenkins.security.stapler.StaplerDispatchable;
|
||||
import jenkins.security.RedactSecretJsonInErrorMessageSanitizer;
|
||||
import jenkins.security.stapler.TypedFilter;
|
||||
import jenkins.util.SystemProperties;
|
||||
import hudson.cli.declarative.CLIMethod;
|
||||
import hudson.cli.declarative.CLIResolver;
|
||||
@@ -899,6 +903,16 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
|
||||
webApp.setClassLoader(pluginManager.uberClassLoader);
|
||||
webApp.setJsonInErrorMessageSanitizer(RedactSecretJsonInErrorMessageSanitizer.INSTANCE);
|
||||
|
||||
TypedFilter typedFilter = new TypedFilter();
|
||||
webApp.setFilterForGetMethods(typedFilter);
|
||||
webApp.setFilterForFields(typedFilter);
|
||||
webApp.setFilterForDoActions(new DoActionFilter());
|
||||
|
||||
StaplerFilteredActionListener actionListener = new StaplerFilteredActionListener();
|
||||
webApp.setFilteredGetterTriggerListener(actionListener);
|
||||
webApp.setFilteredDoActionTriggerListener(actionListener);
|
||||
webApp.setFilteredFieldTriggerListener(actionListener);
|
||||
|
||||
adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader,"adjuncts/"+SESSION_HASH, TimeUnit.DAYS.toMillis(365));
|
||||
|
||||
ClassFilterImpl.register();
|
||||
@@ -1648,6 +1662,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
|
||||
save();
|
||||
}
|
||||
|
||||
@StaplerDispatchable
|
||||
public FederatedLoginService getFederatedLoginService(String name) {
|
||||
for (FederatedLoginService fls : FederatedLoginService.all()) {
|
||||
if (fls.getUrlName().equals(name))
|
||||
@@ -2573,6 +2588,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
|
||||
*
|
||||
* @since 1.349
|
||||
*/
|
||||
@StaplerDispatchable
|
||||
public ExtensionList getExtensionList(String extensionType) throws ClassNotFoundException {
|
||||
return getExtensionList(pluginManager.uberClassLoader.loadClass(extensionType));
|
||||
}
|
||||
@@ -2942,6 +2958,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
|
||||
}
|
||||
|
||||
// if no finger print matches, display "not found page".
|
||||
@StaplerDispatchable
|
||||
public Object getFingerprint( String md5sum ) throws IOException {
|
||||
Fingerprint r = fingerprintMap.get(md5sum);
|
||||
if(r==null) return new NoFingerprintMatch(md5sum);
|
||||
@@ -4090,6 +4107,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
|
||||
* End point that intentionally throws an exception to test the error behaviour.
|
||||
* @since 1.467
|
||||
*/
|
||||
@StaplerDispatchable
|
||||
public void doException() {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
@@ -4648,6 +4666,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
|
||||
* Plugins who wish to contribute boxes on the side panel can add widgets
|
||||
* by {@code getWidgets().add(new MyWidget())} from {@link Plugin#start()}.
|
||||
*/
|
||||
@StaplerDispatchable // some plugins use this to add views to widgets
|
||||
public List<Widget> getWidgets() {
|
||||
return widgets;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import hudson.ExtensionList;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
import org.kohsuke.stapler.Function;
|
||||
import org.kohsuke.stapler.FunctionList;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Restricted(NoExternalUse.class)
|
||||
public class DoActionFilter implements FunctionList.Filter {
|
||||
private static final Logger LOGGER = Logger.getLogger(DoActionFilter.class.getName());
|
||||
|
||||
/**
|
||||
* if a method has "do" as name (not possible in pure Java but doable in Groovy or other JVM languages)
|
||||
* the new system does not consider it as a web method.
|
||||
* <p>
|
||||
* Use <code>@WebMethod(name="")</code> or <code>doIndex</code> in such case.
|
||||
*/
|
||||
private static final Pattern DO_METHOD_REGEX = Pattern.compile("^do[^a-z].*");
|
||||
|
||||
public boolean keep(@Nonnull Function m) {
|
||||
|
||||
if (m.getAnnotation(StaplerNotDispatchable.class) != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m.getAnnotation(StaplerDispatchable.class) != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String methodName = m.getName();
|
||||
String signature = m.getSignature();
|
||||
|
||||
// check whitelist
|
||||
ExtensionList<RoutingDecisionProvider> whitelistProviders = ExtensionList.lookup(RoutingDecisionProvider.class);
|
||||
if (whitelistProviders.size() > 0) {
|
||||
for (RoutingDecisionProvider provider : whitelistProviders) {
|
||||
RoutingDecisionProvider.Decision methodDecision = provider.decide(signature);
|
||||
if (methodDecision == RoutingDecisionProvider.Decision.ACCEPTED) {
|
||||
LOGGER.log(Level.CONFIG, "Action " + signature + " is acceptable because it is whitelisted by " + provider);
|
||||
return true;
|
||||
}
|
||||
if (methodDecision == RoutingDecisionProvider.Decision.REJECTED) {
|
||||
LOGGER.log(Level.CONFIG, "Action " + signature + " is not acceptable because it is blacklisted by " + provider);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (methodName.equals("doDynamic")) {
|
||||
// reject doDynamic because it's treated separately by Stapler.
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Annotation a : m.getAnnotations()) {
|
||||
if (WebMethodConstants.WEB_METHOD_ANNOTATION_NAMES.contains(a.annotationType().getName())) {
|
||||
return true;
|
||||
}
|
||||
if (a.annotationType().getAnnotation(InterceptorAnnotation.class) != null) {
|
||||
// This is a Stapler interceptor annotation like RequirePOST or JsonResponse
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// there is rarely more than two annotations in a method signature
|
||||
for (Annotation[] perParameterAnnotation : m.getParameterAnnotations()) {
|
||||
for (Annotation annotation : perParameterAnnotation) {
|
||||
if (WebMethodConstants.WEB_METHOD_PARAMETER_ANNOTATION_NAMES.contains(annotation.annotationType().getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!DO_METHOD_REGEX.matcher(methodName).matches()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// after the method name check to avoid allowing methods that are meant to be used by routable ones
|
||||
// normally they should be private in such case
|
||||
for (Class<?> parameterType : m.getParameterTypes()) {
|
||||
if (WebMethodConstants.WEB_METHOD_PARAMETERS_NAMES.contains(parameterType.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Class<?> returnType = m.getReturnType();
|
||||
if (HttpResponse.class.isAssignableFrom(returnType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// as HttpResponseException inherits from RuntimeException,
|
||||
// there is no requirement for the developer to explicitly checks it.
|
||||
Class<?>[] checkedExceptionTypes = m.getCheckedExceptionTypes();
|
||||
for (Class<?> checkedExceptionType : checkedExceptionTypes) {
|
||||
if (HttpResponse.class.isAssignableFrom(checkedExceptionType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import hudson.ExtensionPoint;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public abstract class RoutingDecisionProvider implements ExtensionPoint {
|
||||
enum Decision {
|
||||
ACCEPTED,
|
||||
REJECTED,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
@Nonnull public abstract Decision decide(@Nonnull String signature);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
import org.kohsuke.stapler.Function;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.kohsuke.stapler.StaplerResponse;
|
||||
import org.kohsuke.stapler.event.FilteredDoActionTriggerListener;
|
||||
import org.kohsuke.stapler.event.FilteredFieldTriggerListener;
|
||||
import org.kohsuke.stapler.event.FilteredGetterTriggerListener;
|
||||
import org.kohsuke.stapler.lang.FieldRef;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Log a warning message when a "getter" or "doAction" function that was filtered out by SECURITY-400 new rules
|
||||
*/
|
||||
@Restricted(NoExternalUse.class)
|
||||
public class StaplerFilteredActionListener implements FilteredDoActionTriggerListener, FilteredGetterTriggerListener, FilteredFieldTriggerListener {
|
||||
private static final Logger LOGGER = Logger.getLogger(StaplerFilteredActionListener.class.getName());
|
||||
|
||||
private static final String LOG_MESSAGE = "New Stapler routing rules result in the URL \"{0}\" no longer being allowed. " +
|
||||
"If you consider it safe to use, add the following to the whitelist: \"{1}\". " +
|
||||
"Learn more: https://jenkins.io/redirect/stapler-routing";
|
||||
|
||||
@Override
|
||||
public boolean onDoActionTrigger(Function f, StaplerRequest req, StaplerResponse rsp, Object node) {
|
||||
LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{
|
||||
req.getPathInfo(),
|
||||
f.getSignature()
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGetterTrigger(Function f, StaplerRequest req, StaplerResponse rsp, Object node, String expression) {
|
||||
LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{
|
||||
req.getPathInfo(),
|
||||
f.getSignature()
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFieldTrigger(FieldRef f, StaplerRequest req, StaplerResponse staplerResponse, Object node, String expression) {
|
||||
LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{
|
||||
req.getPathInfo(),
|
||||
f.getSignature()
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import hudson.BulkChange;
|
||||
import hudson.Extension;
|
||||
import hudson.ExtensionList;
|
||||
import hudson.model.Saveable;
|
||||
import jenkins.model.Jenkins;
|
||||
import jenkins.util.SystemProperties;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
import org.kohsuke.stapler.Function;
|
||||
import org.kohsuke.stapler.WebApp;
|
||||
import org.kohsuke.stapler.lang.FieldRef;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Fill the list of getter methods that are whitelisted for Stapler
|
||||
* Each item in the set are formatted to correspond exactly to what {@link Function#getDisplayName()} returns
|
||||
*/
|
||||
@Restricted(NoExternalUse.class)
|
||||
@Extension
|
||||
public class StaticRoutingDecisionProvider extends RoutingDecisionProvider implements Saveable {
|
||||
private static final Logger LOGGER = Logger.getLogger(StaticRoutingDecisionProvider.class.getName());
|
||||
|
||||
private Set<String> whitelistSignaturesFromFixedList;
|
||||
private Set<String> whitelistSignaturesFromUserControlledList;
|
||||
|
||||
private Set<String> blacklistSignaturesFromFixedList;
|
||||
private Set<String> blacklistSignaturesFromUserControlledList;
|
||||
|
||||
public StaticRoutingDecisionProvider() {
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the singleton instance of this class, typically for script console use
|
||||
*/
|
||||
public static StaticRoutingDecisionProvider get() {
|
||||
return ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Function#getSignature()
|
||||
* @see FieldRef#getSignature()
|
||||
*/
|
||||
@Nonnull
|
||||
public synchronized Decision decide(@Nonnull String signature) {
|
||||
if (whitelistSignaturesFromFixedList == null || whitelistSignaturesFromUserControlledList == null ||
|
||||
blacklistSignaturesFromFixedList == null || blacklistSignaturesFromUserControlledList == null) {
|
||||
reload();
|
||||
}
|
||||
|
||||
LOGGER.log(Level.CONFIG, "Checking whitelist for " + signature);
|
||||
|
||||
// priority to blacklist
|
||||
if (blacklistSignaturesFromFixedList.contains(signature) || blacklistSignaturesFromUserControlledList.contains(signature)) {
|
||||
return Decision.REJECTED;
|
||||
}
|
||||
|
||||
if (whitelistSignaturesFromFixedList.contains(signature) || whitelistSignaturesFromUserControlledList.contains(signature)) {
|
||||
return Decision.ACCEPTED;
|
||||
}
|
||||
|
||||
return Decision.UNKNOWN;
|
||||
}
|
||||
|
||||
public synchronized void reload() {
|
||||
reloadFromDefault();
|
||||
reloadFromUserControlledList();
|
||||
|
||||
resetMetaClassCache();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
synchronized void resetAndSave(){
|
||||
this.whitelistSignaturesFromFixedList = new HashSet<>();
|
||||
this.whitelistSignaturesFromUserControlledList = new HashSet<>();
|
||||
this.blacklistSignaturesFromFixedList = new HashSet<>();
|
||||
this.blacklistSignaturesFromUserControlledList = new HashSet<>();
|
||||
|
||||
this.save();
|
||||
}
|
||||
|
||||
private void resetMetaClassCache() {
|
||||
// to allow the change to be effective, i.e. rebuild the MetaClass using the new whitelist
|
||||
WebApp.get(Jenkins.get().servletContext).clearMetaClassCache();
|
||||
}
|
||||
|
||||
private synchronized void reloadFromDefault() {
|
||||
try (InputStream is = StaticRoutingDecisionProvider.class.getResourceAsStream("default-whitelist.txt")) {
|
||||
whitelistSignaturesFromFixedList = new HashSet<>();
|
||||
blacklistSignaturesFromFixedList = new HashSet<>();
|
||||
|
||||
parseFileIntoList(
|
||||
IOUtils.readLines(is, StandardCharsets.UTF_8),
|
||||
whitelistSignaturesFromFixedList,
|
||||
blacklistSignaturesFromFixedList
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
|
||||
LOGGER.log(Level.FINE, "Found {0} getter in the standard whitelist", whitelistSignaturesFromFixedList.size());
|
||||
}
|
||||
|
||||
public synchronized StaticRoutingDecisionProvider add(@Nonnull String signature) {
|
||||
if (this.whitelistSignaturesFromUserControlledList.add(signature)) {
|
||||
LOGGER.log(Level.INFO, "Signature [{0}] added to the whitelist", signature);
|
||||
save();
|
||||
resetMetaClassCache();
|
||||
} else {
|
||||
LOGGER.log(Level.INFO, "Signature [{0}] was already present in the whitelist", signature);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized StaticRoutingDecisionProvider addBlacklistSignature(@Nonnull String signature) {
|
||||
if (this.blacklistSignaturesFromUserControlledList.add(signature)) {
|
||||
LOGGER.log(Level.INFO, "Signature [{0}] added to the blacklist", signature);
|
||||
save();
|
||||
resetMetaClassCache();
|
||||
} else {
|
||||
LOGGER.log(Level.INFO, "Signature [{0}] was already present in the blacklist", signature);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized StaticRoutingDecisionProvider remove(@Nonnull String signature) {
|
||||
if (this.whitelistSignaturesFromUserControlledList.remove(signature)) {
|
||||
LOGGER.log(Level.INFO, "Signature [{0}] removed from the whitelist", signature);
|
||||
save();
|
||||
resetMetaClassCache();
|
||||
} else {
|
||||
LOGGER.log(Level.INFO, "Signature [{0}] was not present in the whitelist", signature);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized StaticRoutingDecisionProvider removeBlacklistSignature(@Nonnull String signature) {
|
||||
if (this.blacklistSignaturesFromUserControlledList.remove(signature)) {
|
||||
LOGGER.log(Level.INFO, "Signature [{0}] removed from the blacklist", signature);
|
||||
save();
|
||||
resetMetaClassCache();
|
||||
} else {
|
||||
LOGGER.log(Level.INFO, "Signature [{0}] was not present in the blacklist", signature);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the configuration info to the disk.
|
||||
*/
|
||||
public synchronized void save() {
|
||||
if (BulkChange.contains(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
File file = getConfigFile();
|
||||
try {
|
||||
List<String> allSignatures = new ArrayList<>(whitelistSignaturesFromUserControlledList);
|
||||
blacklistSignaturesFromUserControlledList.stream()
|
||||
.map(signature -> "!" + signature)
|
||||
.forEach(allSignatures::add);
|
||||
|
||||
FileUtils.writeLines(file, allSignatures);
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.WARNING, "Failed to save " + file.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the data from the disk into this object.
|
||||
*
|
||||
* <p>
|
||||
* The constructor of the derived class must call this method.
|
||||
* (If we do that in the base class, the derived class won't
|
||||
* get a chance to set default values.)
|
||||
*/
|
||||
private synchronized void reloadFromUserControlledList() {
|
||||
File file = getConfigFile();
|
||||
if (!file.exists()) {
|
||||
if ((whitelistSignaturesFromUserControlledList != null && whitelistSignaturesFromUserControlledList.isEmpty()) ||
|
||||
(blacklistSignaturesFromUserControlledList != null && blacklistSignaturesFromUserControlledList.isEmpty())) {
|
||||
LOGGER.log(Level.INFO, "No whitelist source file found at " + file + " so resetting user-controlled whitelist");
|
||||
}
|
||||
whitelistSignaturesFromUserControlledList = new HashSet<>();
|
||||
blacklistSignaturesFromUserControlledList = new HashSet<>();
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.log(Level.INFO, "Whitelist source file found at " + file);
|
||||
|
||||
try {
|
||||
whitelistSignaturesFromUserControlledList = new HashSet<>();
|
||||
blacklistSignaturesFromUserControlledList = new HashSet<>();
|
||||
|
||||
parseFileIntoList(
|
||||
FileUtils.readLines(file, StandardCharsets.UTF_8),
|
||||
whitelistSignaturesFromUserControlledList,
|
||||
blacklistSignaturesFromUserControlledList
|
||||
);
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.WARNING, "Failed to load " + file.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private File getConfigFile() {
|
||||
return new File(WHITELIST_PATH == null ? new File(Jenkins.get().getRootDir(), "stapler-whitelist.txt").toString() : WHITELIST_PATH);
|
||||
}
|
||||
|
||||
private void parseFileIntoList(List<String> lines, Set<String> whitelist, Set<String> blacklist){
|
||||
lines.stream()
|
||||
.filter(line -> !line.matches("#.*|\\s*"))
|
||||
.forEach(line -> {
|
||||
if (line.startsWith("!")) {
|
||||
String withoutExclamation = line.substring(1);
|
||||
if (!withoutExclamation.isEmpty()) {
|
||||
blacklist.add(withoutExclamation);
|
||||
}
|
||||
} else {
|
||||
whitelist.add(line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Allow script console access */
|
||||
public static String WHITELIST_PATH = SystemProperties.getString(StaticRoutingDecisionProvider.class.getName() + ".whitelist");
|
||||
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import hudson.ExtensionList;
|
||||
import jenkins.util.SystemProperties;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
import org.kohsuke.stapler.Function;
|
||||
import org.kohsuke.stapler.FunctionList;
|
||||
import org.kohsuke.stapler.StaplerFallback;
|
||||
import org.kohsuke.stapler.StaplerOverridable;
|
||||
import org.kohsuke.stapler.StaplerProxy;
|
||||
import org.kohsuke.stapler.WebApp;
|
||||
import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
|
||||
import org.kohsuke.stapler.lang.FieldRef;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@Restricted(NoExternalUse.class)
|
||||
public class TypedFilter implements FieldRef.Filter, FunctionList.Filter {
|
||||
private static final Logger LOGGER = Logger.getLogger(TypedFilter.class.getName());
|
||||
|
||||
private static final Map<Class<?>, Boolean> staplerCache = new HashMap<>();
|
||||
|
||||
private boolean isClassAcceptable(Class<?> clazz) {
|
||||
if (clazz.isArray()) {
|
||||
// special case to allow klass.isArray() dispatcher
|
||||
Class<?> elementClazz = clazz.getComponentType();
|
||||
// does not seem possible to fall in an infinite loop since array cannot be recursively defined
|
||||
if (isClassAcceptable(elementClazz)) {
|
||||
LOGGER.log(Level.FINE,
|
||||
"Class {0} is acceptable because it is an Array of acceptable elements {1}",
|
||||
new Object[]{clazz.getName(), elementClazz.getName()}
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
LOGGER.log(Level.FINE,
|
||||
"Class {0} is not acceptable because it is an Array of non-acceptable elements {1}",
|
||||
new Object[]{clazz.getName(), elementClazz.getName()}
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return SKIP_TYPE_CHECK || isStaplerRelevantCached(clazz);
|
||||
}
|
||||
|
||||
private static boolean isStaplerRelevantCached(@Nonnull Class<?> clazz) {
|
||||
if (staplerCache.containsKey(clazz)) {
|
||||
return staplerCache.get(clazz);
|
||||
}
|
||||
boolean ret = isStaplerRelevant(clazz);
|
||||
|
||||
staplerCache.put(clazz, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Restricted(NoExternalUse.class)
|
||||
public static boolean isStaplerRelevant(@Nonnull Class<?> clazz) {
|
||||
return isSpecificClassStaplerRelevant(clazz) || isSuperTypesStaplerRelevant(clazz);
|
||||
}
|
||||
|
||||
private static boolean isSuperTypesStaplerRelevant(@Nonnull Class<?> clazz) {
|
||||
Class<?> superclass = clazz.getSuperclass();
|
||||
if (superclass != null && isStaplerRelevantCached(superclass)) {
|
||||
return true;
|
||||
}
|
||||
for (Class<?> interfaceClass : clazz.getInterfaces()) {
|
||||
if (isStaplerRelevantCached(interfaceClass)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isSpecificClassStaplerRelevant(@Nonnull Class<?> clazz) {
|
||||
if (clazz.isAnnotationPresent(StaplerAccessibleType.class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Classes implementing these Stapler types can be considered routable
|
||||
if (StaplerProxy.class.isAssignableFrom(clazz)) {
|
||||
return true;
|
||||
}
|
||||
if (StaplerFallback.class.isAssignableFrom(clazz)) {
|
||||
return true;
|
||||
}
|
||||
if (StaplerOverridable.class.isAssignableFrom(clazz)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (Method m : clazz.getMethods()) {
|
||||
if (isRoutableMethod(m)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isRoutableMethod(@Nonnull Method m) {
|
||||
for (Annotation a : m.getDeclaredAnnotations()) {
|
||||
if (WebMethodConstants.WEB_METHOD_ANNOTATION_NAMES.contains(a.annotationType().getName())) {
|
||||
return true;
|
||||
}
|
||||
if (a.annotationType().isAnnotationPresent(InterceptorAnnotation.class)) {
|
||||
// This is a Stapler interceptor annotation like RequirePOST or JsonResponse
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (Annotation[] set : m.getParameterAnnotations()) {
|
||||
for (Annotation a : set) {
|
||||
if (WebMethodConstants.WEB_METHOD_PARAMETER_ANNOTATION_NAMES.contains(a.annotationType().getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Class<?> parameterType : m.getParameterTypes()) {
|
||||
if (WebMethodConstants.WEB_METHOD_PARAMETERS_NAMES.contains(parameterType.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return WebApp.getCurrent().getFilterForDoActions().keep(new Function.InstanceFunction(m));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keep(@Nonnull FieldRef fieldRef) {
|
||||
|
||||
if (fieldRef.getAnnotation(StaplerNotDispatchable.class) != null) {
|
||||
// explicitly marked as an invalid field
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fieldRef.getAnnotation(StaplerDispatchable.class) != null) {
|
||||
// explicitly marked as a valid field
|
||||
return true;
|
||||
}
|
||||
|
||||
String signature = fieldRef.getSignature();
|
||||
|
||||
// check whitelist
|
||||
ExtensionList<RoutingDecisionProvider> decisionProviders = ExtensionList.lookup(RoutingDecisionProvider.class);
|
||||
if (decisionProviders.size() > 0) {
|
||||
for (RoutingDecisionProvider provider : decisionProviders) {
|
||||
RoutingDecisionProvider.Decision fieldDecision = provider.decide(signature);
|
||||
if (fieldDecision == RoutingDecisionProvider.Decision.ACCEPTED) {
|
||||
LOGGER.log(Level.CONFIG, "Field {0} is acceptable because it is whitelisted by {1}", new Object[]{signature, provider});
|
||||
return true;
|
||||
}
|
||||
if (fieldDecision == RoutingDecisionProvider.Decision.REJECTED) {
|
||||
LOGGER.log(Level.CONFIG, "Field {0} is not acceptable because it is blacklisted by {1}", new Object[]{signature, provider});
|
||||
return false;
|
||||
}
|
||||
Class<?> type = fieldRef.getReturnType();
|
||||
if (type != null) {
|
||||
String typeSignature = "class " + type.getCanonicalName();
|
||||
RoutingDecisionProvider.Decision fieldTypeDecision = provider.decide(typeSignature);
|
||||
if (fieldTypeDecision == RoutingDecisionProvider.Decision.ACCEPTED) {
|
||||
LOGGER.log(Level.CONFIG, "Field {0} is acceptable because its type is whitelisted by {1}", new Object[]{signature, provider});
|
||||
return true;
|
||||
}
|
||||
if (fieldTypeDecision == RoutingDecisionProvider.Decision.REJECTED) {
|
||||
LOGGER.log(Level.CONFIG, "Field {0} is not acceptable because its type is blacklisted by {1}", new Object[]{signature, provider});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (PROHIBIT_STATIC_ACCESS && fieldRef.isStatic()) {
|
||||
// unless whitelisted or marked as routable, reject static fields
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Class<?> returnType = fieldRef.getReturnType();
|
||||
|
||||
boolean isOk = isClassAcceptable(returnType);
|
||||
LOGGER.log(Level.FINE, "Field analyzed: {0} => {1}", new Object[]{fieldRef.getName(), isOk});
|
||||
return isOk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keep(@Nonnull Function function) {
|
||||
|
||||
if (function.getAnnotation(StaplerNotDispatchable.class) != null) {
|
||||
// explicitly marked as an invalid getter
|
||||
return false;
|
||||
}
|
||||
|
||||
if (function.getAnnotation(StaplerDispatchable.class) != null) {
|
||||
// explicitly marked as a valid getter
|
||||
return true;
|
||||
}
|
||||
|
||||
String signature = function.getSignature();
|
||||
|
||||
// check whitelist
|
||||
ExtensionList<RoutingDecisionProvider> decision = ExtensionList.lookup(RoutingDecisionProvider.class);
|
||||
if (decision.size() > 0) {
|
||||
for (RoutingDecisionProvider provider : decision) {
|
||||
RoutingDecisionProvider.Decision methodDecision = provider.decide(signature);
|
||||
if (methodDecision == RoutingDecisionProvider.Decision.ACCEPTED) {
|
||||
LOGGER.log(Level.CONFIG, "Function {0} is acceptable because it is whitelisted by {1}", new Object[]{signature, provider});
|
||||
return true;
|
||||
}
|
||||
if (methodDecision == RoutingDecisionProvider.Decision.REJECTED) {
|
||||
LOGGER.log(Level.CONFIG, "Function {0} is not acceptable because it is blacklisted by {1}", new Object[]{signature, provider});
|
||||
return false;
|
||||
}
|
||||
|
||||
Class<?> type = function.getReturnType();
|
||||
if (type != null) {
|
||||
String typeSignature = "class " + type.getCanonicalName();
|
||||
RoutingDecisionProvider.Decision returnTypeDecision = provider.decide(typeSignature);
|
||||
if (returnTypeDecision == RoutingDecisionProvider.Decision.ACCEPTED) {
|
||||
LOGGER.log(Level.CONFIG, "Function {0} is acceptable because its type is whitelisted by {1}", new Object[]{signature, provider});
|
||||
return true;
|
||||
}
|
||||
if (returnTypeDecision == RoutingDecisionProvider.Decision.REJECTED) {
|
||||
LOGGER.log(Level.CONFIG, "Function {0} is not acceptable because its type is blacklisted by {1}", new Object[]{signature, provider});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (PROHIBIT_STATIC_ACCESS && function.isStatic()) {
|
||||
// unless whitelisted or marked as routable, reject static methods
|
||||
return false;
|
||||
}
|
||||
|
||||
if (function.getName().equals("getDynamic")) {
|
||||
Class[] parameterTypes = function.getParameterTypes();
|
||||
if (parameterTypes.length > 0 && parameterTypes[0] == String.class) {
|
||||
// While this is more general than what Stapler can invoke on these types,
|
||||
// The above is the only criterion for Stapler to attempt dispatch.
|
||||
// Therefore prohibit this as a regular getter.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (function.getName().equals("getStaplerFallback") && function.getParameterTypes().length == 0) {
|
||||
// A parameter-less #getStaplerFallback() implements special fallback behavior for the
|
||||
// StaplerFallback interface. We do not check for the presence of the interface on the current
|
||||
// class, or the return type, as that could change since the implementing component was last built.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (function.getName().equals("getTarget") && function.getParameterTypes().length == 0) {
|
||||
// A parameter-less #getTarget() implements special redirection behavior for the
|
||||
// StaplerProxy interface. We do not check for the presence of the interface on the current
|
||||
// class, or the return type, as that could change since the implementing component was last built.
|
||||
return false;
|
||||
}
|
||||
|
||||
Class<?> returnType = function.getReturnType();
|
||||
|
||||
boolean isOk = isClassAcceptable(returnType);
|
||||
LOGGER.log(Level.FINE, "Function analyzed: {0} => {1}", new Object[]{signature, isOk});
|
||||
return isOk;
|
||||
}
|
||||
|
||||
@Restricted(NoExternalUse.class)
|
||||
public static boolean SKIP_TYPE_CHECK = SystemProperties.getBoolean(TypedFilter.class.getName() + ".skipTypeCheck");
|
||||
|
||||
@Restricted(NoExternalUse.class)
|
||||
public static boolean PROHIBIT_STATIC_ACCESS = SystemProperties.getBoolean(TypedFilter.class.getName() + ".prohibitStaticAccess", true);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
import org.kohsuke.stapler.AncestorInPath;
|
||||
import org.kohsuke.stapler.Header;
|
||||
import org.kohsuke.stapler.QueryParameter;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.kohsuke.stapler.StaplerResponse;
|
||||
import org.kohsuke.stapler.WebMethod;
|
||||
import org.kohsuke.stapler.bind.JavaScriptMethod;
|
||||
import org.kohsuke.stapler.json.JsonBody;
|
||||
import org.kohsuke.stapler.json.SubmittedForm;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Restricted(NoExternalUse.class)
|
||||
final class WebMethodConstants {
|
||||
/**
|
||||
* If a method has at least one of those parameters, it is considered as an implicit web method
|
||||
*/
|
||||
private static final List<Class<?>> WEB_METHOD_PARAMETERS = Collections.unmodifiableList(Arrays.asList(
|
||||
StaplerRequest.class,
|
||||
HttpServletRequest.class,
|
||||
StaplerResponse.class,
|
||||
HttpServletResponse.class
|
||||
));
|
||||
|
||||
static final Set<String> WEB_METHOD_PARAMETERS_NAMES = Collections.unmodifiableSet(
|
||||
WEB_METHOD_PARAMETERS.stream()
|
||||
.map(Class::getName)
|
||||
.collect(Collectors.toSet())
|
||||
);
|
||||
|
||||
/**
|
||||
* If a method is annotated with one of those annotations,
|
||||
* the method is considered as an explicit web method
|
||||
*/
|
||||
static final List<Class<? extends Annotation>> WEB_METHOD_ANNOTATIONS = Collections.singletonList(
|
||||
WebMethod.class
|
||||
// plus every annotation that's annotated with InterceptorAnnotation
|
||||
// JavaScriptMethod.class not taken here because it's a special case
|
||||
);
|
||||
|
||||
static final Set<String> WEB_METHOD_ANNOTATION_NAMES;
|
||||
static {
|
||||
Set<String> webMethodAnnotationNames = WEB_METHOD_ANNOTATIONS.stream()
|
||||
.map(Class::getName)
|
||||
.collect(Collectors.toSet());
|
||||
webMethodAnnotationNames.add(JavaScriptMethod.class.getName());
|
||||
WEB_METHOD_ANNOTATION_NAMES = Collections.unmodifiableSet(webMethodAnnotationNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* If at least one parameter of the method is annotated with one of those annotations,
|
||||
* the method is considered as an implicit web method
|
||||
*/
|
||||
private static final List<Class<? extends Annotation>> WEB_METHOD_PARAMETER_ANNOTATIONS = Collections.unmodifiableList(Arrays.asList(
|
||||
QueryParameter.class,
|
||||
AncestorInPath.class,
|
||||
Header.class,
|
||||
JsonBody.class,
|
||||
SubmittedForm.class
|
||||
));
|
||||
|
||||
static final Set<String> WEB_METHOD_PARAMETER_ANNOTATION_NAMES = Collections.unmodifiableSet(
|
||||
WEB_METHOD_PARAMETER_ANNOTATIONS.stream()
|
||||
.map(Class::getName)
|
||||
.collect(Collectors.toSet())
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
# This file contains the built-in whitelist for Stapler request dispatching.
|
||||
# It's a tool for retaining compatibility with unusual plugin behavior after introducing the SECURITY-595 security fix.
|
||||
# To provide your own custom whitelist, create/edit $JENKINS_HOME/stapler-whitelist.txt
|
||||
|
||||
# Determine the whitelist entries for methods in a known class from the script console:
|
||||
# com.acme.package.ClassName.class.methods.each {
|
||||
# println new org.kohsuke.stapler.Function.InstanceFunction(it).signature
|
||||
# }
|
||||
# com.acme.package.ClassName.class.fields.each {
|
||||
# println org.kohsuke.stapler.lang.FieldRef.wrap(it).signature
|
||||
# }
|
||||
# return
|
||||
|
||||
#######################################################################################################################
|
||||
###################################################### Whitelist ######################################################
|
||||
#######################################################################################################################
|
||||
|
||||
|
||||
######################
|
||||
# Credentials Plugin #
|
||||
######################
|
||||
# Used where credentials are used (e.g. SCM config), without this, 'Add' button will break as its dialog is at:
|
||||
# /descriptor/….CredentialsSelectHelper/resolver/….CredentialsSelectHelper$SystemContextResolver/provider/….SystemCredentialsProvider$ProviderImpl/context/jenkins/dialog
|
||||
method com.cloudbees.plugins.credentials.CredentialsSelectHelper getResolver java.lang.String
|
||||
method com.cloudbees.plugins.credentials.CredentialsSelectHelper$WrappedContextResolver getProvider java.lang.String
|
||||
# Used by Credentials Plugin's FingerprintTest and Git Plugin's GitSCMTest, as well as others:
|
||||
method com.cloudbees.plugins.credentials.CredentialsStoreAction$DomainWrapper getCredentials
|
||||
|
||||
|
||||
################
|
||||
# JUnit Plugin #
|
||||
################
|
||||
# Allow various #getHistory() as these only have resources and #getGraph()
|
||||
class hudson.tasks.junit.History
|
||||
|
||||
|
||||
##################
|
||||
# Metrics Plugin #
|
||||
##################
|
||||
# Method returns Object for no clear reason
|
||||
method jenkins.metrics.api.MetricsRootAction getCurrentUser
|
||||
|
||||
|
||||
#########################
|
||||
# Pipeline Plugin Suite #
|
||||
#########################
|
||||
# Used in the 'Pipeline Steps' UI, the Execution has Nodes but no UI of its own
|
||||
method org.jenkinsci.plugins.workflow.job.WorkflowRun getExecution
|
||||
# FlowGraphTable only has a Jelly view, and nothing else that would indicate Stapler-routability
|
||||
method org.jenkinsci.plugins.workflow.job.views.FlowGraphTableAction getFlowGraph
|
||||
|
||||
|
||||
############################
|
||||
# Maven Integration Plugin #
|
||||
############################
|
||||
# Advertised in https://github.com/jenkinsci/maven-plugin/blob/7ac83fa85fda0c4d1d02663059644f0655823879/src/main/resources/hudson/maven/reporters/MavenArtifactRecord/_api.jelly#L31
|
||||
field hudson.maven.reporters.MavenArtifactRecord attachedArtifacts
|
||||
|
||||
|
||||
###########################################
|
||||
# Static Analysis Plugins (analysis-core) #
|
||||
###########################################
|
||||
# Methods return Object for no clear reason
|
||||
method hudson.plugins.analysis.core.AbstractProjectAction getTrendGraph
|
||||
method hudson.plugins.analysis.core.AbstractProjectAction getTrendDetails
|
||||
method hudson.plugins.analysis.core.AbstractProjectAction getTrendDetails org.kohsuke.stapler.StaplerRequest org.kohsuke.stapler.StaplerResponse
|
||||
|
||||
|
||||
###########################
|
||||
# Blue Ocean Plugin Suite #
|
||||
###########################
|
||||
# Methods return Object for no clear reason
|
||||
method io.jenkins.blueocean.service.embedded.rest.AbstractRunImpl getLog
|
||||
method io.jenkins.blueocean.service.embedded.rest.QueuedBlueRun getLog
|
||||
method io.jenkins.blueocean.rest.impl.pipeline.PipelineNodeImpl getLog
|
||||
method io.jenkins.blueocean.rest.impl.pipeline.PipelineStepImpl getLog
|
||||
|
||||
|
||||
##########################
|
||||
# Promoted Builds Plugin #
|
||||
##########################
|
||||
# Only subtypes look Stapler-relevant
|
||||
method hudson.plugins.promoted_builds.PromotionProcess getPromotionCondition java.lang.String
|
||||
|
||||
|
||||
##########################
|
||||
# Robot Framework Plugin #
|
||||
##########################
|
||||
# Unsure whether this is needed, but RobotSuiteResult is supposed to be reachable.
|
||||
method hudson.plugins.robot.model.RobotResult getSuites
|
||||
|
||||
|
||||
########################
|
||||
# Maven Invoker Plugin #
|
||||
########################
|
||||
# MavenInvokerResult only has an index view, and nothing else that would indicate Stapler-routability
|
||||
method org.jenkinsci.plugins.maveninvoker.MavenInvokerBuildAction getResult java.lang.String
|
||||
|
||||
|
||||
###########################
|
||||
# Cloud Statistics Plugin #
|
||||
###########################
|
||||
# Used via CloudStatistics#getUrl(ProvisioningActivity, PhaseExecution, PhaseExecutionAttachment) for attempts.groovy
|
||||
method org.jenkinsci.plugins.cloudstats.CloudStatistics getActivity java.lang.String
|
||||
method org.jenkinsci.plugins.cloudstats.ProvisioningActivity getPhase java.lang.String
|
||||
|
||||
|
||||
####################
|
||||
# SLOCCount Plugin #
|
||||
####################
|
||||
# Support various getters in the same type
|
||||
class hudson.plugins.sloccount.SloccountResult
|
||||
|
||||
|
||||
##############################
|
||||
# Project Inheritance Plugin #
|
||||
##############################
|
||||
# do* methods with no indication they're supposed to be routable (return String, no args, no annotations)
|
||||
method hudson.plugins.project_inheritance.projects.InheritanceProject doGetParamDefaultsAsXML
|
||||
method hudson.plugins.project_inheritance.projects.InheritanceProject doGetParamExpansionsAsXML
|
||||
method hudson.plugins.project_inheritance.projects.InheritanceProject doGetVersionsAsCompressedXML
|
||||
method hudson.plugins.project_inheritance.projects.InheritanceProject doGetVersionsAsXML
|
||||
method hudson.plugins.project_inheritance.projects.InheritanceProject doRenderSVGRelationGraph
|
||||
|
||||
|
||||
###############################################
|
||||
# Changes since last successfull build Plugin # (sic)
|
||||
###############################################
|
||||
# Only has an index.jelly view, so needs to be explicit
|
||||
class com.cloudbees.jenkins.plugins.changelog.Changes
|
||||
|
||||
|
||||
###################
|
||||
# Fitnesse Plugin #
|
||||
###################
|
||||
# return hudson.plugins.fitnesse.History with only getters for hudson.util.Graph, like analysis-core
|
||||
method hudson.plugins.fitnesse.FitnesseProjectAction getTrend
|
||||
|
||||
|
||||
###########################
|
||||
# Build Time Blame Plugin #
|
||||
###########################
|
||||
# Only has resources and a getter for a Graph, so needs whitelisting when not scanning
|
||||
class org.jenkins.ci.plugins.buildtimeblame.analysis.BlameReport
|
||||
|
||||
|
||||
##########################
|
||||
# Azure VM Agents Plugin #
|
||||
##########################
|
||||
# Declared to return String, no args, and always returns null…?
|
||||
method com.microsoft.azure.vmagent.AzureVMAgentTemplate$DescriptorImpl doFillImageReferenceTypeItems
|
||||
|
||||
|
||||
################################
|
||||
# TestComplete support plug-in #
|
||||
################################
|
||||
method com.smartbear.jenkins.plugins.testcomplete.TcSummaryAction getReports
|
||||
|
||||
|
||||
#####################
|
||||
# Job Cacher Plugin #
|
||||
#####################
|
||||
# UI linking to this is ArbitraryFileCache/cache-entry.jelly, used from CacheProjectAction/index.jelly, and ultimately handled by ArbitraryFileCache#doDynamic
|
||||
method jenkins.plugins.jobcacher.CacheProjectAction getCaches
|
||||
|
||||
|
||||
#########################
|
||||
# Dimensions SCM Plugin #
|
||||
#########################
|
||||
# Needs whitelisting due to not following the usual naming scheme
|
||||
method hudson.plugins.dimensionsscm.DimensionsSCM$DescriptorImpl domanadatoryFieldCheck org.kohsuke.stapler.StaplerRequest org.kohsuke.stapler.StaplerResponse
|
||||
|
||||
|
||||
##############################
|
||||
# Google Health Check Plugin #
|
||||
##############################
|
||||
method com.google.jenkins.plugins.health.lib.DerivedPageAction getZone java.lang.String
|
||||
@@ -0,0 +1,116 @@
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.kohsuke.stapler.Function;
|
||||
import org.kohsuke.stapler.QueryParameter;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.kohsuke.stapler.StaplerResponse;
|
||||
import org.kohsuke.stapler.json.JsonResponse;
|
||||
import org.kohsuke.stapler.lang.FieldRef;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class StaplerSignaturesTest {
|
||||
@Test
|
||||
public void testSignaturesSimple() throws Exception {
|
||||
Set<String> methodSignatures = Arrays.stream(SomeClass.class.getMethods()).map(it -> new Function.InstanceFunction(it).getSignature()).collect(Collectors.toSet());
|
||||
Assert.assertEquals(SomeClass.METHOD_SIGNATURES, methodSignatures);
|
||||
|
||||
Set<String> fieldSignatures = Arrays.stream(SomeClass.class.getFields()).map(it -> FieldRef.wrap(it).getSignature()).collect(Collectors.toSet());
|
||||
Assert.assertEquals(SomeClass.FIELD_SIGNATURES, fieldSignatures);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignaturesInheritance() throws Exception {
|
||||
Set<String> methodSignatures = Arrays.stream(SomeSubclass.class.getMethods()).map(it -> new Function.InstanceFunction(it).getSignature()).collect(Collectors.toSet());
|
||||
Assert.assertEquals(SomeSubclass.METHOD_SIGNATURES, methodSignatures);
|
||||
|
||||
Set<String> fieldSignatures = Arrays.stream(SomeSubclass.class.getFields()).map(it -> FieldRef.wrap(it).getSignature()).collect(Collectors.toSet());
|
||||
Assert.assertEquals(SomeSubclass.FIELD_SIGNATURES, fieldSignatures);
|
||||
}
|
||||
|
||||
public static class SomeClass {
|
||||
static Set<String> METHOD_SIGNATURES = new HashSet<>(Arrays.asList(
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo",
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo java.lang.String",
|
||||
"staticMethod jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo int",
|
||||
"staticMethod jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo long",
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo jenkins.security.stapler.StaplerSignaturesTest$SomeClass",
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeClass doFoo org.kohsuke.stapler.StaplerRequest org.kohsuke.stapler.StaplerResponse",
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeClass doWhatever java.lang.String",
|
||||
"method java.lang.Object getClass",
|
||||
"method java.lang.Object equals java.lang.Object",
|
||||
"method java.lang.Object hashCode",
|
||||
"method java.lang.Object notify",
|
||||
"method java.lang.Object notifyAll",
|
||||
"method java.lang.Object toString",
|
||||
"method java.lang.Object wait long int",
|
||||
"method java.lang.Object wait long",
|
||||
"method java.lang.Object wait"
|
||||
));
|
||||
public void getFoo() {}
|
||||
public void getFoo(String arg) {}
|
||||
public static void getFoo(int arg) {}
|
||||
public static void getFoo(long arg) {}
|
||||
public void getFoo(SomeClass arg) {}
|
||||
public void doFoo(StaplerRequest req, StaplerResponse rsp) {}
|
||||
@StaplerDispatchable @JsonResponse
|
||||
public void doWhatever(@QueryParameter String arg) {}
|
||||
|
||||
static Set<String> FIELD_SIGNATURES = new HashSet<>(Arrays.asList(
|
||||
"field jenkins.security.stapler.StaplerSignaturesTest$SomeClass whatever",
|
||||
"field jenkins.security.stapler.StaplerSignaturesTest$SomeClass thing",
|
||||
"staticField jenkins.security.stapler.StaplerSignaturesTest$SomeClass staticField",
|
||||
"field jenkins.security.stapler.StaplerSignaturesTest$SomeClass stringList"
|
||||
));
|
||||
public String whatever;
|
||||
public Object thing;
|
||||
public static Object staticField;
|
||||
public List<String> stringList;
|
||||
|
||||
}
|
||||
|
||||
public static class SomeSubclass extends SomeClass {
|
||||
static Set<String> METHOD_SIGNATURES = new HashSet<>(Arrays.asList(
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass getFoo",
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass subtypeExclusive",
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass subtypeExclusive java.lang.String",
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass varargMethod [Ljava.lang.String;",
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo java.lang.String",
|
||||
"staticMethod jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo int",
|
||||
"staticMethod jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo long",
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeClass getFoo jenkins.security.stapler.StaplerSignaturesTest$SomeClass",
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeClass doFoo org.kohsuke.stapler.StaplerRequest org.kohsuke.stapler.StaplerResponse",
|
||||
"method jenkins.security.stapler.StaplerSignaturesTest$SomeClass doWhatever java.lang.String",
|
||||
"method java.lang.Object getClass",
|
||||
"method java.lang.Object equals java.lang.Object",
|
||||
"method java.lang.Object hashCode",
|
||||
"method java.lang.Object notify",
|
||||
"method java.lang.Object notifyAll",
|
||||
"method java.lang.Object toString",
|
||||
"method java.lang.Object wait long int",
|
||||
"method java.lang.Object wait long",
|
||||
"method java.lang.Object wait"
|
||||
));
|
||||
public void getFoo() {}
|
||||
public void subtypeExclusive(){}
|
||||
public void subtypeExclusive(String arg){}
|
||||
public void varargMethod(String... args){}
|
||||
|
||||
static Set<String> FIELD_SIGNATURES = new HashSet<>(Arrays.asList(
|
||||
"field jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass whatever",
|
||||
"field jenkins.security.stapler.StaplerSignaturesTest$SomeClass whatever",
|
||||
"field jenkins.security.stapler.StaplerSignaturesTest$SomeClass thing",
|
||||
"staticField jenkins.security.stapler.StaplerSignaturesTest$SomeSubclass staticField",
|
||||
"staticField jenkins.security.stapler.StaplerSignaturesTest$SomeClass staticField",
|
||||
"field jenkins.security.stapler.StaplerSignaturesTest$SomeClass stringList"
|
||||
));
|
||||
public String whatever;
|
||||
public static Object staticField;
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,12 @@ THE SOFTWARE.
|
||||
<version>2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jenkins-ci.plugins</groupId>
|
||||
<artifactId>cloudbees-folder</artifactId>
|
||||
<version>6.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>maven-plugin</artifactId>
|
||||
|
||||
@@ -23,10 +23,13 @@
|
||||
*/
|
||||
package hudson.model;
|
||||
|
||||
import com.cloudbees.hudson.plugins.folder.Folder;
|
||||
import com.gargoylesoftware.htmlunit.WebRequest;
|
||||
import com.gargoylesoftware.htmlunit.html.DomNodeUtil;
|
||||
import com.gargoylesoftware.htmlunit.util.NameValuePair;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.jenkins.ui.icon.Icon;
|
||||
import org.jenkins.ui.icon.IconSet;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
|
||||
import com.gargoylesoftware.htmlunit.HttpMethod;
|
||||
@@ -194,6 +197,18 @@ public class ViewTest {
|
||||
@Issue("JENKINS-9367")
|
||||
@Test public void allImagesCanBeLoaded() throws Exception {
|
||||
User.get("user", true);
|
||||
|
||||
// as long as the cloudbees-folder is included as test dependency, its Folder will load icon
|
||||
boolean folderPluginActive = (j.jenkins.getPlugin("cloudbees-folder") != null);
|
||||
// link to Folder class is done here to ensure if we remove the dependency, this code will fail and so will be removed
|
||||
boolean folderPluginClassesLoaded = (j.jenkins.getDescriptor(Folder.class) != null);
|
||||
// this could be written like this to avoid the hard dependency:
|
||||
// boolean folderPluginClassesLoaded = (j.jenkins.getDescriptor("com.cloudbees.hudson.plugins.folder.Folder") != null);
|
||||
if (!folderPluginActive && folderPluginClassesLoaded) {
|
||||
// reset the icon added by Folder because the plugin resources are not reachable
|
||||
IconSet.icons.addIcon(new Icon("icon-folder icon-md", "24x24/folder.gif", "width: 24px; height: 24px;"));
|
||||
}
|
||||
|
||||
WebClient webClient = j.createWebClient();
|
||||
webClient.getOptions().setJavaScriptEnabled(false);
|
||||
j.assertAllImageLoadSuccessfully(webClient.goTo("asynchPeople"));
|
||||
|
||||
@@ -59,7 +59,7 @@ public class FormFieldValidatorTest {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void doCheckXyz() {
|
||||
public FormValidation doCheckXyz() {
|
||||
throw new Error("doCheckXyz is broken");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.Page;
|
||||
import hudson.model.UnprotectedRootAction;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.For;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.TestExtension;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
import org.kohsuke.stapler.StaplerResponse;
|
||||
import org.kohsuke.stapler.WebMethod;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
@Issue("SECURITY-400")
|
||||
@For(RoutingDecisionProvider.class)
|
||||
public class CustomRoutingDecisionProviderTest {
|
||||
|
||||
@Rule
|
||||
public JenkinsRule j = new JenkinsRule();
|
||||
|
||||
@TestExtension("customRoutingWhitelistProvider")
|
||||
public static class XxxBlacklister extends RoutingDecisionProvider {
|
||||
@Override
|
||||
public Decision decide(@Nonnull String signature) {
|
||||
if (signature.contains("xxx")) {
|
||||
return Decision.REJECTED;
|
||||
}
|
||||
return Decision.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class OneMethodIsBlacklisted implements UnprotectedRootAction {
|
||||
@Override
|
||||
public @CheckForNull String getUrlName() {
|
||||
return "custom";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIconFileName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public StaplerAbstractTest.Renderable getLegitGetter() {
|
||||
return new StaplerAbstractTest.Renderable();
|
||||
}
|
||||
|
||||
public StaplerAbstractTest.Renderable getLegitxxxGetter() {
|
||||
return new StaplerAbstractTest.Renderable();
|
||||
}
|
||||
}
|
||||
|
||||
private static class Renderable {
|
||||
public void doIndex() {replyOk();}
|
||||
|
||||
@WebMethod(name = "valid")
|
||||
public void valid() {replyOk();}
|
||||
}
|
||||
|
||||
private static void replyOk() {
|
||||
StaplerResponse resp = Stapler.getCurrentResponse();
|
||||
try {
|
||||
resp.getWriter().write("ok");
|
||||
resp.flushBuffer();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customRoutingWhitelistProvider() throws Exception {
|
||||
Page okPage = j.createWebClient().goTo("custom/legitGetter", null);
|
||||
assertThat(okPage.getWebResponse().getStatusCode(), is(200));
|
||||
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
Page errorPage = wc.goTo("custom/legitxxxGetter", null);
|
||||
assertThat(errorPage.getWebResponse().getStatusCode(), is(404));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,738 @@
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
|
||||
import com.gargoylesoftware.htmlunit.HttpMethod;
|
||||
import com.gargoylesoftware.htmlunit.Page;
|
||||
import com.gargoylesoftware.htmlunit.WebRequest;
|
||||
import com.gargoylesoftware.htmlunit.util.NameValuePair;
|
||||
import net.sf.json.JSONArray;
|
||||
import net.sf.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.TestExtension;
|
||||
import org.kohsuke.stapler.AncestorInPath;
|
||||
import org.kohsuke.stapler.CapturedParameterNames;
|
||||
import org.kohsuke.stapler.CrumbIssuer;
|
||||
import org.kohsuke.stapler.Header;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
import org.kohsuke.stapler.HttpResponses;
|
||||
import org.kohsuke.stapler.LimitedTo;
|
||||
import org.kohsuke.stapler.QueryParameter;
|
||||
import org.kohsuke.stapler.RequestImpl;
|
||||
import org.kohsuke.stapler.ResponseImpl;
|
||||
import org.kohsuke.stapler.StaplerProxy;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.kohsuke.stapler.StaplerResponse;
|
||||
import org.kohsuke.stapler.WebMethod;
|
||||
import org.kohsuke.stapler.bind.JavaScriptMethod;
|
||||
import org.kohsuke.stapler.interceptor.JsonOutputFilter;
|
||||
import org.kohsuke.stapler.interceptor.RequirePOST;
|
||||
import org.kohsuke.stapler.interceptor.RespondSuccess;
|
||||
import org.kohsuke.stapler.json.JsonBody;
|
||||
import org.kohsuke.stapler.json.JsonResponse;
|
||||
import org.kohsuke.stapler.json.SubmittedForm;
|
||||
import org.kohsuke.stapler.verb.DELETE;
|
||||
import org.kohsuke.stapler.verb.GET;
|
||||
import org.kohsuke.stapler.verb.POST;
|
||||
import org.kohsuke.stapler.verb.PUT;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* To check the previous behavior you can use:
|
||||
* <pre>
|
||||
* {@link org.kohsuke.stapler.MetaClass#LEGACY_WEB_METHOD_MODE} = true;
|
||||
* </pre>
|
||||
* It will disable the usage of {@link DoActionFilter}
|
||||
*/
|
||||
@Issue("SECURITY-400")
|
||||
public class DoActionFilterTest extends StaplerAbstractTest {
|
||||
|
||||
@TestExtension
|
||||
public static class TestAccessModifierUrl extends AbstractUnprotectedRootAction {
|
||||
public TestAccessModifier getPublic() {return new TestAccessModifier();}
|
||||
|
||||
protected TestAccessModifier getProtected() {return new TestAccessModifier();}
|
||||
|
||||
TestAccessModifier getInternal() {return new TestAccessModifier();}
|
||||
|
||||
private TestAccessModifier getPrivate() {return new TestAccessModifier();}
|
||||
|
||||
public class TestAccessModifier {
|
||||
@GET
|
||||
public String doValue() {
|
||||
return "hello";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProtectedMethodDispatch() throws Exception {
|
||||
try {
|
||||
wc.goTo("testAccessModifierUrl/public/value", null);
|
||||
} catch (FailingHttpStatusCodeException e) {
|
||||
fail("should have access to a public method");
|
||||
}
|
||||
try {
|
||||
wc.goTo("testAccessModifierUrl/protected/value", null);
|
||||
fail("should not have allowed protected access");
|
||||
} catch (FailingHttpStatusCodeException x) {
|
||||
assertEquals(HttpServletResponse.SC_NOT_FOUND, x.getStatusCode());
|
||||
}
|
||||
try {
|
||||
wc.goTo("testAccessModifierUrl/internal/value", null);
|
||||
fail("should not have allowed internal access");
|
||||
} catch (FailingHttpStatusCodeException x) {
|
||||
assertEquals(HttpServletResponse.SC_NOT_FOUND, x.getStatusCode());
|
||||
}
|
||||
try {
|
||||
wc.goTo("testAccessModifierUrl/private/value", null);
|
||||
fail("should not have allowed private access");
|
||||
} catch (FailingHttpStatusCodeException x) {
|
||||
assertEquals(HttpServletResponse.SC_NOT_FOUND, x.getStatusCode());
|
||||
}
|
||||
}
|
||||
|
||||
//================================= doXxx methods =================================
|
||||
|
||||
@TestExtension
|
||||
public static class TestNewRulesOk extends AbstractUnprotectedRootAction {
|
||||
/*
|
||||
* Method signature
|
||||
*/
|
||||
|
||||
public static void doStaticWithRequest(StaplerRequest request) { replyOk(); }
|
||||
|
||||
public void doWithRequest(StaplerRequest request) { replyOk(); }
|
||||
|
||||
public void doWithHttpRequest(HttpServletRequest request) { replyOk(); }
|
||||
|
||||
// the return type is not taken into consideration if it's not a HttpResponse, it will not prevent the method
|
||||
// to be considered as a web method
|
||||
public String doWithRequestAndReturnString(StaplerRequest request) { return "ok"; }
|
||||
|
||||
public void doWithResponse(StaplerResponse response) { replyOk(); }
|
||||
|
||||
public void doWithHttpResponse(HttpServletResponse response) { replyOk(); }
|
||||
|
||||
public void doWithThrowHttpResponseException() throws HttpResponses.HttpResponseException { replyOk(); }
|
||||
|
||||
// special cases, child of above classes, normally reachable, as it satisfies the contract
|
||||
// that requires to throw an exception that is an HttpResponseException
|
||||
public void doWithThrowHttpResponseExceptionChild() throws HttpResponseExceptionChild { replyOk(); }
|
||||
|
||||
// the declared exception just has to implement HttpResponse
|
||||
public void doWithThrowExceptionImplementingOnlyHttpResponse() throws ExceptionImplementingOnlyHttpResponse { replyOk(); }
|
||||
|
||||
public void doWithThrowOtherException() throws IOException { replyOk(); }
|
||||
|
||||
public HttpResponse doWithReturnHttpResponse() { return HttpResponses.plainText("ok"); }
|
||||
|
||||
public HttpResponseChild doWithReturnHttpResponseChild() { return new HttpResponseChild(); }
|
||||
|
||||
/*
|
||||
* Method annotations
|
||||
*/
|
||||
|
||||
@WebMethod(name = "webMethodUrl")
|
||||
public void doWebMethod() { replyOk(); }
|
||||
|
||||
// not requiring to have doXxx when using WebMethod
|
||||
@WebMethod(name = "webMethodUrl2")
|
||||
public void webMethod() { replyOk(); }
|
||||
|
||||
@GET
|
||||
public void doAnnotatedGet() { replyOk(); }
|
||||
|
||||
@POST
|
||||
public void doAnnotatedPost() { replyOk(); }
|
||||
|
||||
@PUT
|
||||
public void doAnnotatedPut() { replyOk(); }
|
||||
|
||||
@DELETE
|
||||
public void doAnnotatedDelete() { replyOk(); }
|
||||
|
||||
@RequirePOST
|
||||
public void doAnnotatedRequirePost() { replyOk(); }
|
||||
|
||||
@JavaScriptMethod
|
||||
public void annotatedJavascriptScriptMethod() { replyOk(); }
|
||||
|
||||
@RespondSuccess
|
||||
public void doAnnotatedResponseSuccess() { replyOk(); }
|
||||
|
||||
@JsonResponse // does not support list
|
||||
public Map<String, Object> doAnnotatedJsonResponse() {
|
||||
return new HashMap<String, Object>() {{
|
||||
put("a", "b");
|
||||
}};
|
||||
}
|
||||
|
||||
@LimitedTo("admin")
|
||||
public void doAnnotatedLimitedTo() { replyOk(); }
|
||||
|
||||
/*
|
||||
* Parameter annotation
|
||||
*/
|
||||
|
||||
public void doAnnotatedParamQueryParameter(@QueryParameter String value) { replyOk(); }
|
||||
|
||||
public void doAnnotatedParamAncestorInPath(@AncestorInPath DoActionFilterTest parent) { replyOk(); }
|
||||
|
||||
public void doAnnotatedParamHeader(@Header("test-header") String testHeader) { replyOk(); }
|
||||
|
||||
public void doAnnotatedParamJsonBody(@JsonBody Map<String, String> names) { replyOk(); }
|
||||
|
||||
public void doAnnotatedParamSubmittedForm(@SubmittedForm JSONObject form) { replyOk(); }
|
||||
|
||||
/*
|
||||
* Parameter annotation
|
||||
*/
|
||||
|
||||
public void do_CallMeBecauseOfMyUnderscore(StaplerRequest request) { replyOk(); }
|
||||
|
||||
public void do$CallMeBecauseOfMyDollar(StaplerRequest request) { replyOk(); }
|
||||
}
|
||||
|
||||
public static class HttpResponseChild implements HttpResponse {
|
||||
@Override
|
||||
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
|
||||
replyOk();
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class HttpResponseExceptionChild extends HttpResponses.HttpResponseException {
|
||||
}
|
||||
|
||||
public static class ExceptionImplementingOnlyHttpResponse extends RuntimeException implements HttpResponse {
|
||||
@Override
|
||||
public void generateResponse(StaplerRequest staplerRequest, StaplerResponse staplerResponse, Object o) throws IOException, ServletException {
|
||||
replyOk();
|
||||
}
|
||||
}
|
||||
|
||||
//########### actual test methods ###########
|
||||
@Test
|
||||
public void testMethodSignatureOk_staticWithRequest() throws Exception {
|
||||
assertReachable("testNewRulesOk/staticWithRequest/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSignatureOk_withRequest() throws Exception {
|
||||
assertReachable("testNewRulesOk/withRequest/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSignatureOk_withRequestAndReturnString() throws Exception {
|
||||
assertReachable("testNewRulesOk/withRequestAndReturnString/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSignatureOk_withHttpRequest() throws Exception {
|
||||
assertReachable("testNewRulesOk/withHttpRequest/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSignatureOk_withHttpResponse() throws Exception {
|
||||
assertReachable("testNewRulesOk/withHttpResponse/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSignatureOk_withResponse() throws Exception {
|
||||
assertReachable("testNewRulesOk/withResponse/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSignatureOk_withThrowHttpResponseException() throws Exception {
|
||||
assertReachable("testNewRulesOk/withThrowHttpResponseException/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSignatureOk_withThrowHttpResponseExceptionChild() throws Exception {
|
||||
assertReachable("testNewRulesOk/withThrowHttpResponseExceptionChild/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSignatureOk_withThrowExceptionImplementingOnlyHttpResponse() throws Exception {
|
||||
assertReachable("testNewRulesOk/withThrowExceptionImplementingOnlyHttpResponse/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSignatureOk_withThrowOtherException() throws Exception {
|
||||
assertNotReachable("testNewRulesOk/withThrowOtherException/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSignatureOk_withReturnHttpResponse() throws Exception {
|
||||
assertReachable("testNewRulesOk/withReturnHttpResponse/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSignatureOk_withReturnHttpResponseChild() throws Exception {
|
||||
assertReachable("testNewRulesOk/withReturnHttpResponseChild/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodOk_webMethodUrl() throws Exception {
|
||||
assertReachable("testNewRulesOk/webMethodUrl/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodOk_webMethodUrl2() throws Exception {
|
||||
assertReachable("testNewRulesOk/webMethodUrl2/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodOk_annotatedGet() throws Exception {
|
||||
assertReachable("testNewRulesOk/annotatedGet/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodOk_annotatedPost() throws Exception {
|
||||
WebRequest settings = new WebRequest(new URL(j.getURL(), "testNewRulesOk/annotatedPost/"));
|
||||
settings.setHttpMethod(HttpMethod.POST);
|
||||
settings.setRequestBody("");
|
||||
assertReachableWithSettings(settings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodOk_annotatedPut() throws Exception {
|
||||
WebRequest settings = new WebRequest(new URL(j.getURL(), "testNewRulesOk/annotatedPut/"));
|
||||
settings.setHttpMethod(HttpMethod.PUT);
|
||||
settings.setRequestBody("");
|
||||
assertReachableWithSettings(settings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodOk_annotatedDelete() throws Exception {
|
||||
assertReachable("testNewRulesOk/annotatedDelete/", HttpMethod.DELETE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodOk_annotatedRequirePost() throws Exception {
|
||||
WebRequest settings = new WebRequest(new URL(j.getURL(), "testNewRulesOk/annotatedRequirePost/"));
|
||||
settings.setHttpMethod(HttpMethod.POST);
|
||||
settings.setRequestBody("");
|
||||
assertReachableWithSettings(settings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodOk_annotatedJavascriptScriptMethod() throws Exception {
|
||||
webApp.setCrumbIssuer(new CrumbIssuer() {
|
||||
@Override
|
||||
public String issueCrumb(StaplerRequest request) {
|
||||
return "test";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateCrumb(StaplerRequest request, String submittedCrumb) {
|
||||
// no exception thrown = validated
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
WebRequest settings = new WebRequest(new URL(j.getURL(), "testNewRulesOk/annotatedJavascriptScriptMethod/"));
|
||||
settings.setAdditionalHeader("Content-Type", "application/x-stapler-method-invocation");
|
||||
settings.setHttpMethod(HttpMethod.POST);
|
||||
settings.setRequestBody(JSONArray.fromObject(Arrays.asList()).toString());
|
||||
assertReachableWithSettings(settings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodOk_annotatedResponseSuccess() throws Exception {
|
||||
assertReachable("testNewRulesOk/annotatedResponseSuccess/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodOk_annotatedJsonResponse() throws Exception {
|
||||
WebRequest settings = new WebRequest(new URL(j.getURL(), "testNewRulesOk/annotatedJsonResponse/"));
|
||||
settings.setHttpMethod(HttpMethod.POST);
|
||||
settings.setRequestBody(JSONObject.fromObject(Collections.emptyMap()).toString());
|
||||
Page page = wc.getPage(settings);
|
||||
assertEquals(200, page.getWebResponse().getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodOk_annotatedLimitedTo() throws Exception {
|
||||
try {
|
||||
wc.getPage(new URL(j.getURL(), "testNewRulesOk/annotatedLimitedTo/"));
|
||||
fail();
|
||||
} catch (FailingHttpStatusCodeException e) {
|
||||
assertEquals(500, e.getStatusCode());
|
||||
assertTrue(e.getResponse().getContentAsString().contains("Needs to be in role"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedParameterOk_annotatedParamQueryParameter() throws Exception {
|
||||
// parameter is optional by default
|
||||
assertReachable("testNewRulesOk/annotatedParamQueryParameter/");
|
||||
assertReachable("testNewRulesOk/annotatedParamQueryParameter/?value=test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedParameterOk_annotatedParamAncestorInPath() throws Exception {
|
||||
assertReachable("testNewRulesOk/annotatedParamAncestorInPath/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedParameterOk_annotatedParamHeader() throws Exception {
|
||||
WebRequest settings = new WebRequest(new URL(j.getURL(), "testNewRulesOk/annotatedParamHeader/"));
|
||||
settings.setAdditionalHeader("test-header", "TestBrowser");
|
||||
assertReachableWithSettings(settings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedParameterOk_annotatedParamJsonBody() throws Exception {
|
||||
WebRequest settings = new WebRequest(new URL(j.getURL(), "testNewRulesOk/annotatedParamJsonBody/"));
|
||||
// WebClient forces us to use POST to have the possibility to send requestBody
|
||||
settings.setHttpMethod(HttpMethod.POST);
|
||||
settings.setAdditionalHeader("Content-Type", "application/json");
|
||||
settings.setRequestBody(JSONObject.fromObject(new HashMap<String, Object>() {{
|
||||
put("name", "Test");
|
||||
}}).toString());
|
||||
assertReachableWithSettings(settings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedParameterOk_annotatedParamSubmittedForm() throws Exception {
|
||||
WebRequest settings = new WebRequest(new URL(j.getURL(), "testNewRulesOk/annotatedParamSubmittedForm/"));
|
||||
settings.setHttpMethod(HttpMethod.POST);
|
||||
|
||||
settings.setRequestParameters(Arrays.asList(
|
||||
new NameValuePair(
|
||||
"json",
|
||||
JSONObject.fromObject(new HashMap<String, Object>() {{
|
||||
put("name", "Test");
|
||||
}}).toString()
|
||||
)
|
||||
));
|
||||
assertReachableWithSettings(settings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOk__CallMeBecauseOfMyUnderscore() throws Exception {
|
||||
assertReachable("testNewRulesOk/_CallMeBecauseOfMyUnderscore/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOk_$CallMeBecauseOfMyDollar() throws Exception {
|
||||
assertReachable("testNewRulesOk/$CallMeBecauseOfMyDollar/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class TestNewRulesOkDynamic extends AbstractUnprotectedRootAction {
|
||||
// sufficiently magical name to be reached
|
||||
public void doDynamic() { replyOk(); }
|
||||
}
|
||||
|
||||
|
||||
@TestExtension
|
||||
public static class TestNewRulesOkIndex extends AbstractUnprotectedRootAction {
|
||||
// considered as index
|
||||
@WebMethod(name = "")
|
||||
public void methodWithoutNameEqualIndex() { replyOk(); }
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class TestNewRulesOkDoIndex extends AbstractUnprotectedRootAction {
|
||||
public void doIndex() { replyOk(); }
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpecialCasesOk() throws Exception {
|
||||
assertReachable("testNewRulesOkDynamic/anyString/");
|
||||
assertReachable("testNewRulesOkIndex/");
|
||||
assertReachable("testNewRulesOkDoIndex/");
|
||||
}
|
||||
|
||||
// those methods are accepted in legacy system but potentially dangerous
|
||||
@TestExtension
|
||||
public static class TestNewRulesNotOk extends AbstractUnprotectedRootAction {
|
||||
// do not respect the do[^a-z].* format
|
||||
public void dontCallMeBecauseOfMyDont(StaplerRequest request) { replyOk(); }
|
||||
|
||||
// do not seem to be an expected web method, in case a developer has such methods,
|
||||
// addition of WebMethod annotation is sufficient
|
||||
public void doSomething() { replyOk(); }
|
||||
|
||||
// returning a String is not sufficient to be considered as a web method
|
||||
public String doReturnString() { return "ok"; }
|
||||
|
||||
// returning a super class of HttpResponse is not sufficient
|
||||
public Object doReturnObject() { return "ok"; }
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotOk_ntCallMeBecauseOfMyDont() throws Exception {
|
||||
assertNotReachable("testNewRulesNotOk/ntCallMeBecauseOfMyDont/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotOk_something() throws Exception {
|
||||
assertNotReachable("testNewRulesNotOk/something/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotOk_returnString() throws Exception {
|
||||
assertNotReachable("testNewRulesNotOk/returnString/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotOk_returnObject() throws Exception {
|
||||
assertNotReachable("testNewRulesNotOk/returnObject/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class TestNewRulesNotOkSpecialCases extends AbstractUnprotectedRootAction {
|
||||
public void doWithServletRequest(ServletRequest request) { replyOk(); }
|
||||
|
||||
public void doWithServletResponse(ServletResponse response) { replyOk(); }
|
||||
|
||||
// special cases, child of above classes
|
||||
public void doWithRequestImpl(RequestImpl request) { replyOk(); }
|
||||
|
||||
public void doWithResponseImpl(ResponseImpl response) { replyOk(); }
|
||||
|
||||
public void doWithRequestAndResponse(RequestAndResponse requestAndResponse) { replyOk(); }
|
||||
|
||||
// special case to keep Groovy parameter name, but does not seem to indicate it's automatically a web method
|
||||
@CapturedParameterNames({"req"})
|
||||
public void doAnnotatedResponseSuccess(Object req) { replyOk(); }
|
||||
|
||||
// // as mentioned in its documentation, it requires to have JavaScriptMethod, that has its own test
|
||||
// @JsonOutputFilter
|
||||
// public void doAnnotatedJsonOutputFilter() { replyOk(); }
|
||||
}
|
||||
|
||||
public static abstract class RequestAndResponse implements StaplerRequest, StaplerResponse {
|
||||
@Override
|
||||
public CollectionAndEnumeration getHeaderNames() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CollectionAndEnumeration getHeaders(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static abstract class CollectionAndEnumeration implements Collection, Enumeration {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotOkSpecialCases_withServletRequest() throws Exception {
|
||||
assertNotReachable("testNewRulesNotOkSpecialCases/withServletRequest/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotOkSpecialCases_withServletResponse() throws Exception {
|
||||
assertNotReachable("testNewRulesNotOkSpecialCases/withServletResponse/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotOkSpecialCases_withRequestImpl() throws Exception {
|
||||
assertNotReachable("testNewRulesNotOkSpecialCases/withRequestImpl/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotOkSpecialCases_withResponseImpl() throws Exception {
|
||||
assertNotReachable("testNewRulesNotOkSpecialCases/withResponseImpl/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotOkSpecialCases_withRequestAndResponse() throws Exception {
|
||||
assertNotReachable("testNewRulesNotOkSpecialCases/withRequestAndResponse/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotOkSpecialCases_annotatedResponseSuccess() throws Exception {
|
||||
assertNotReachable("testNewRulesNotOkSpecialCases/annotatedResponseSuccess/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
// now JsonOutputFilter is accepted as a web method annotation
|
||||
// @Test
|
||||
// public void testNotOkSpecialCases_annotatedJsonOutputFilter() throws Exception {
|
||||
// assertNotReachable("testNewRulesNotOkSpecialCases/annotatedJsonOutputFilter/");
|
||||
// assertDoActionRequestWasBlockedAndResetFlag();
|
||||
// }
|
||||
|
||||
//================================= class inheritance =================================
|
||||
|
||||
public static class A {
|
||||
public void doNotAnnotatedAtAll() { replyOk(); }
|
||||
|
||||
@WebMethod(name = "onlyAnnotatedInA")
|
||||
public void doOnlyAnnotatedInA() { replyOk(); }
|
||||
|
||||
public void doOnlyAnnotatedInB() { replyOk(); }
|
||||
|
||||
@WebMethod(name = "onlyAnnotatedInA-notOverrided")
|
||||
public void doOnlyAnnotatedInANotOverrided() { replyOk(); }
|
||||
|
||||
@WebMethod(name = "annotatedButDifferent1")
|
||||
public void doAnnotatedButDifferent() { replyOk(); }
|
||||
}
|
||||
|
||||
public static class B extends A {
|
||||
@Override
|
||||
public void doNotAnnotatedAtAll() { replyOk(); }
|
||||
|
||||
public void doOnlyAnnotatedInA() { replyOk(); }
|
||||
|
||||
@WebMethod(name = "onlyAnnotatedInB")
|
||||
public void doOnlyAnnotatedInB() { replyOk(); }
|
||||
|
||||
// doOnlyAnnotatedInANotOverrided: not overrided
|
||||
|
||||
@WebMethod(name = "annotatedButDifferent2")
|
||||
public void doAnnotatedButDifferent() { replyOk(); }
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class ABCase extends AbstractUnprotectedRootAction implements StaplerProxy {
|
||||
@Override
|
||||
public B getTarget() {
|
||||
return new B();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClassInheritance_notAnnotatedAtAll() throws Exception {
|
||||
assertNotReachable("aBCase/notAnnotatedAtAll/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClassInheritance_onlyAnnotatedInA() throws Exception {
|
||||
assertReachable("aBCase/onlyAnnotatedInA/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClassInheritance_onlyAnnotatedInB() throws Exception {
|
||||
assertReachable("aBCase/onlyAnnotatedInB/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClassInheritance_onlyAnnotatedInANotOverrided() throws Exception {
|
||||
assertNotReachable("aBCase/onlyAnnotatedInANotOverrided/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClassInheritance_annotatedButDifferent1() throws Exception {
|
||||
// only the last webMethod annotation is used
|
||||
//TODO it breaks the Liskov substitutability
|
||||
// assertReachable("b/annotatedButDifferent1/");
|
||||
assertNotReachable("aBCase/annotatedButDifferent1/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClassInheritance_annotatedButDifferent2() throws Exception {
|
||||
assertReachable("aBCase/annotatedButDifferent2/");
|
||||
}
|
||||
|
||||
//================================= interface implementation =================================
|
||||
public interface I {
|
||||
void doNotAnnotated();
|
||||
|
||||
@WebMethod(name = "annotatedBoth")
|
||||
void doAnnotatedBoth();
|
||||
|
||||
@WebMethod(name = "annotatedOnlyI")
|
||||
void doAnnotatedOnlyI();
|
||||
|
||||
void doAnnotatedOnlyJ();
|
||||
|
||||
@WebMethod(name = "annotatedButDifferent1")
|
||||
void doAnnotatedButDifferent();
|
||||
}
|
||||
|
||||
public static class J implements I {
|
||||
@Override
|
||||
public void doNotAnnotated() { replyOk(); }
|
||||
|
||||
@Override
|
||||
@WebMethod(name = "annotatedBoth")
|
||||
public void doAnnotatedBoth() { replyOk(); }
|
||||
|
||||
@Override
|
||||
public void doAnnotatedOnlyI() { replyOk(); }
|
||||
|
||||
@Override
|
||||
@WebMethod(name = "annotatedOnlyJ")
|
||||
public void doAnnotatedOnlyJ() { replyOk(); }
|
||||
|
||||
@Override
|
||||
@WebMethod(name = "annotatedButDifferent2")
|
||||
public void doAnnotatedButDifferent() { replyOk(); }
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class IJCase extends AbstractUnprotectedRootAction implements StaplerProxy {
|
||||
@Override
|
||||
public J getTarget() {
|
||||
return new J();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterfaceImplementation_notAnnotated() throws Exception {
|
||||
assertNotReachable("iJCase/notAnnotated/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterfaceImplementation_annotatedBoth() throws Exception {
|
||||
assertReachable("iJCase/annotatedBoth/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterfaceImplementation_annotatedOnlyI() throws Exception {
|
||||
assertReachable("iJCase/annotatedOnlyI/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterfaceImplementation_annotatedOnlyJ() throws Exception {
|
||||
assertReachable("iJCase/annotatedOnlyJ/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterfaceImplementation_annotatedButDifferent1() throws Exception {
|
||||
// only the last webMethod annotation is used
|
||||
//TODO it breaks the Liskov substitutability
|
||||
// assertReachable("j/annotatedButDifferent1/");
|
||||
assertNotReachable("iJCase/annotatedButDifferent1/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterfaceImplementation_annotatedButDifferent2() throws Exception {
|
||||
assertReachable("iJCase/annotatedButDifferent2/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import hudson.model.UnprotectedRootAction;
|
||||
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.TestExtension;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
import org.kohsuke.stapler.HttpResponses;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Issue("SECURITY-400")
|
||||
public class DynamicTest {
|
||||
@Rule
|
||||
public JenkinsRule j = new JenkinsRule();
|
||||
|
||||
@Test
|
||||
public void testRequestsDispatchedToEligibleDynamic() throws Exception {
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
Stream.of("whatever", "displayName", "iconFileName", "urlName", "response1", "response2").forEach(url ->
|
||||
{
|
||||
try {
|
||||
assertThat(wc.goTo("root/" + url).getWebResponse().getContentAsString(), containsString(url));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class Root implements UnprotectedRootAction {
|
||||
|
||||
@CheckForNull
|
||||
@Override
|
||||
public String getIconFileName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@CheckForNull
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public HttpResponse getResponse1() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public HttpResponse doResponse2() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void doDynamic(StaplerRequest req) {
|
||||
throw HttpResponses.errorWithoutStack(200, req.getRestOfPath());
|
||||
}
|
||||
|
||||
@CheckForNull
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return "root";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import com.cloudbees.hudson.plugins.folder.Folder;
|
||||
import hudson.model.TopLevelItem;
|
||||
import hudson.model.View;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.For;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.TestExtension;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
/**
|
||||
* To check the previous behavior you can use:
|
||||
* <pre>
|
||||
* {@link org.kohsuke.stapler.MetaClass#LEGACY_GETTER_MODE} = true;
|
||||
* </pre>
|
||||
* It will disable the usage of {@link TypedFilter}
|
||||
*/
|
||||
@Issue("SECURITY-400")
|
||||
@For(TypedFilter.class)
|
||||
public class GetterMethodFilterTest extends StaplerAbstractTest {
|
||||
|
||||
@TestExtension
|
||||
public static class TestWithReturnJavaPlatformObject extends AbstractUnprotectedRootAction {
|
||||
public static boolean called = false;
|
||||
|
||||
public String getString() { return "a";}
|
||||
|
||||
// cannot provide side-effect since the String has no side-effect methods
|
||||
public Object getObjectString() { return "a";}
|
||||
|
||||
// but it opens wide range of potentially dangerous classes
|
||||
public Object getObjectCustom() {
|
||||
return new Object() {
|
||||
// in order to provide a web entry-point
|
||||
public void doIndex() {
|
||||
replyOk();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public Point getPoint() { return new Point(1, 2);}
|
||||
|
||||
public Point getPointCustomChild() {
|
||||
return new Point() {
|
||||
// in order to provide a web entry-point
|
||||
public void doIndex() {
|
||||
replyOk();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public Point getPointWithListener() {
|
||||
return new Point() {
|
||||
@Override
|
||||
public double getX() {
|
||||
// just to demonstrate the potential side-effect
|
||||
called = true;
|
||||
return super.getX();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnJavaPlatformObject_string() throws Exception {
|
||||
assertNotReachable("testWithReturnJavaPlatformObject/string/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnJavaPlatformObject_objectString() throws Exception {
|
||||
assertNotReachable("testWithReturnJavaPlatformObject/objectString/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnJavaPlatformObject_objectCustom() throws Exception {
|
||||
assertNotReachable("testWithReturnJavaPlatformObject/objectCustom/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnJavaPlatformObject_point() throws Exception {
|
||||
assertNotReachable("testWithReturnJavaPlatformObject/point/");
|
||||
}
|
||||
|
||||
// previously reachable and so potentially open to future security vulnerability
|
||||
@Test
|
||||
public void testWithReturnJavaPlatformObject_pointCustomChild() throws Exception {
|
||||
assertNotReachable("testWithReturnJavaPlatformObject/pointCustomChild/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnJavaPlatformObject_pointWithListener() throws Exception {
|
||||
TestWithReturnJavaPlatformObject.called = false;
|
||||
assertFalse(TestWithReturnJavaPlatformObject.called);
|
||||
// could potentially trigger some side-effects
|
||||
assertNotReachable("testWithReturnJavaPlatformObject/pointWithListener/x/");
|
||||
assertFalse(TestWithReturnJavaPlatformObject.called);
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class TestWithReturnMultiple extends AbstractUnprotectedRootAction {
|
||||
public List<Renderable> getList() {
|
||||
return Arrays.asList(new Renderable(), new Renderable());
|
||||
}
|
||||
|
||||
// as we cannot determine the element class due to type erasure, this is reachable
|
||||
public List<? extends Point> getListOfPoint() {
|
||||
return Collections.singletonList(new RenderablePoint());
|
||||
}
|
||||
|
||||
public List<List<Renderable>> getListOfList() {
|
||||
return Collections.singletonList(Arrays.asList(new Renderable(), new Renderable()));
|
||||
}
|
||||
|
||||
public Renderable[] getArray() { return new Renderable[]{new Renderable(), new Renderable()};}
|
||||
|
||||
// will not be accepted since the componentType is from JVM
|
||||
public Point[] getArrayOfPoint() {
|
||||
return new Point[]{new Point() {
|
||||
public void doIndex() {replyOk();}
|
||||
}};
|
||||
}
|
||||
|
||||
public Renderable[][] getArrayOfArray() {
|
||||
return new Renderable[][]{
|
||||
new Renderable[]{new Renderable(), new Renderable()}
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Renderable>[] getArrayOfList() {
|
||||
List<Renderable> list = Arrays.asList(new Renderable(), new Renderable());
|
||||
return (List<Renderable>[]) Collections.singletonList(list).toArray(new List[0]);
|
||||
}
|
||||
|
||||
public List<Renderable[]> getListOfArray() {
|
||||
return Collections.singletonList(
|
||||
new Renderable[]{new Renderable(), new Renderable()}
|
||||
);
|
||||
}
|
||||
|
||||
public Map<String, Renderable> getMap() {
|
||||
return new HashMap<String, Renderable>() {{
|
||||
put("a", new Renderable());
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnMultiple_list() throws Exception {
|
||||
assertNotReachable("testWithReturnMultiple/list/");
|
||||
assertNotReachable("testWithReturnMultiple/list/0/");
|
||||
assertNotReachable("testWithReturnMultiple/list/1/");
|
||||
assertNotReachable("testWithReturnMultiple/list/2/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnMultiple_listOfPoint() throws Exception {
|
||||
assertNotReachable("testWithReturnMultiple/listOfPoint/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfPoint/0/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfPoint/1/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnMultiple_listOfList() throws Exception {
|
||||
assertNotReachable("testWithReturnMultiple/listOfList/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfList/0/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfList/1/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfList/0/0/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfList/0/1/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfList/0/2/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnMultiple_array() throws Exception {
|
||||
assertNotReachable("testWithReturnMultiple/array/");
|
||||
assertReachable("testWithReturnMultiple/array/0/");
|
||||
assertReachable("testWithReturnMultiple/array/1/");
|
||||
assertNotReachable("testWithReturnMultiple/array/2/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnMultiple_arrayOfPoint() throws Exception {
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfPoint/");
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfPoint/0/");
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfPoint/1/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnMultiple_arrayOfArray() throws Exception {
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfArray/");
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfArray/0/");
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfArray/1/");
|
||||
assertReachable("testWithReturnMultiple/arrayOfArray/0/0/");
|
||||
assertReachable("testWithReturnMultiple/arrayOfArray/0/1/");
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfArray/0/2/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnMultiple_arrayOfList() throws Exception {
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfList/");
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfList/0/");
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfList/1/");
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfList/0/0/");
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfList/0/1/");
|
||||
assertNotReachable("testWithReturnMultiple/arrayOfList/0/2/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnMultiple_listOfArray() throws Exception {
|
||||
assertNotReachable("testWithReturnMultiple/listOfArray/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfArray/0/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfArray/1/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfArray/0/0/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfArray/0/1/");
|
||||
assertNotReachable("testWithReturnMultiple/listOfArray/0/2/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnMultiple_map() throws Exception {
|
||||
assertNotReachable("testWithReturnMultiple/map/");
|
||||
assertNotReachable("testWithReturnMultiple/map/a/");
|
||||
assertNotReachable("testWithReturnMultiple/map/b/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class TestWithReturnCoreObject extends AbstractUnprotectedRootAction {
|
||||
public View.People getPeople() {
|
||||
// provide an index jelly view
|
||||
return new View.People(Jenkins.getInstance());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnCoreObject_people() throws Exception {
|
||||
assertReachableWithoutOk("testWithReturnCoreObject/people/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTopLevelItemIsLegal() throws Exception {
|
||||
TopLevelItem item = j.createFreeStyleProject();
|
||||
assertReachableWithoutOk("job/" + item.getName());
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class TestWithReturnPluginObject extends AbstractUnprotectedRootAction {
|
||||
public Folder getFolder() {
|
||||
return new Folder(Jenkins.getInstance(), "testFolder");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnPluginObject_folder() throws Exception {
|
||||
// the search part is just to get something from the call
|
||||
assertReachableWithoutOk("testWithReturnPluginObject/folder/search/suggest/?query=xxx");
|
||||
}
|
||||
|
||||
// full package name just to be explicit
|
||||
@TestExtension
|
||||
public static class TestWithReturnThirdPartyObject extends AbstractUnprotectedRootAction {
|
||||
public org.apache.commons.codec.binary.Base64 getBase64() {
|
||||
return new org.apache.commons.codec.binary.Base64();
|
||||
}
|
||||
|
||||
public org.apache.commons.codec.Encoder getEncoder() {
|
||||
return new org.apache.commons.codec.binary.Base64();
|
||||
}
|
||||
|
||||
public org.apache.commons.codec.Encoder getEncoderCustomChild() {
|
||||
return new org.apache.commons.codec.Encoder() {
|
||||
@Override
|
||||
public Object encode(Object source) throws org.apache.commons.codec.EncoderException {
|
||||
// it's not about implementation...
|
||||
return null;
|
||||
}
|
||||
|
||||
public void doIndex() {
|
||||
// it's about sending a message
|
||||
replyOk();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// the class itself was reachable but no more interaction are available and so return 404
|
||||
|
||||
@Test
|
||||
public void testWithReturnThirdPartyObject_base32() throws Exception {
|
||||
assertNotReachable("testWithReturnThirdPartyObject/base32/");
|
||||
}
|
||||
|
||||
// the class itself was reachable but no more interaction are available and so return 404,
|
||||
// in case there is some callable methods, we could create some side-effect even we got 404
|
||||
@Test
|
||||
public void testWithReturnThirdPartyObject_encoder() throws Exception {
|
||||
assertNotReachable("testWithReturnThirdPartyObject/encoder/");
|
||||
}
|
||||
|
||||
// as we add a entry-point in the class, now it can propose some interaction,
|
||||
// dangerous behavior that is not prohibited
|
||||
@Test
|
||||
public void testWithReturnThirdPartyObject_encoderCustomChild() throws Exception {
|
||||
assertNotReachable("testWithReturnThirdPartyObject/encoderCustomChild/");
|
||||
}
|
||||
|
||||
|
||||
//================================= getter methods with primitives =================================
|
||||
|
||||
@TestExtension
|
||||
public static class TestWithReturnPrimitives extends AbstractUnprotectedRootAction {
|
||||
public int getInteger() { return 1;}
|
||||
|
||||
public Integer getIntegerObject() { return 1;}
|
||||
|
||||
public long getLong() { return 1L;}
|
||||
|
||||
public Long getLongObject() { return 1L;}
|
||||
|
||||
public short getShort() { return (short) 1;}
|
||||
|
||||
public Short getShortObject() { return 1;}
|
||||
|
||||
public byte getByte() { return (byte) 1;}
|
||||
|
||||
public Byte getByteObject() { return (byte) 1;}
|
||||
|
||||
public boolean getBoolean() { return true;}
|
||||
|
||||
public Boolean getBooleanObject() { return Boolean.TRUE;}
|
||||
|
||||
public char getChar() { return 'a';}
|
||||
|
||||
public Character getCharObject() { return 'a';}
|
||||
|
||||
public float getFloat() { return 1.0f;}
|
||||
|
||||
public Float getFloatObject() { return 1.0f;}
|
||||
|
||||
public double getDouble() { return 1.0;}
|
||||
|
||||
public Double getDoubleObject() { return 1.0;}
|
||||
|
||||
public void getVoid() { }
|
||||
|
||||
public Void getVoidObject() { return null; }
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_integer() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/integer/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_integerObject() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/integerObject/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_long() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/long/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_longObject() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/longObject/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_short() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/short/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_shortObject() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/shortObject/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_byte() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/byte/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_byteObject() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/byteObject/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_boolean() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/boolean/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_booleanObject() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/booleanObject/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_char() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/char/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_charObject() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/charObject/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_float() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/float/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_floatObject() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/floatObject/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_double() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/double/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_doubleObject() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/doubleObject/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_void() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/void/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestWithReturnPrimitives_voidObject() throws Exception {
|
||||
assertNotReachable("testWithReturnPrimitives/voidObject/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
//================================= getter methods =================================
|
||||
|
||||
@TestExtension
|
||||
public static class TestWithReturnWithinStaplerScope extends DoActionFilterTest.AbstractUnprotectedRootAction {
|
||||
public Renderable getRenderable() { return new Renderable();}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithReturnWithinStaplerScope_renderable() throws Exception {
|
||||
assertReachable("testWithReturnWithinStaplerScope/renderable/");
|
||||
assertReachable("testWithReturnWithinStaplerScope/renderable/valid/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.For;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.recipes.WithPlugin;
|
||||
|
||||
@Issue("SECURITY-400")
|
||||
@For({StaplerDispatchable.class, StaplerAccessibleType.class})
|
||||
public class JenkinsSupportAnnotationsTest {
|
||||
|
||||
@Rule
|
||||
public JenkinsRule j = new JenkinsRule();
|
||||
|
||||
@Test
|
||||
@WithPlugin("annotations-test.hpi")
|
||||
public void testPluginWithAnnotations() throws Exception {
|
||||
// test fails if TypedFilter ignores @StaplerDispatchable
|
||||
j.createWebClient().goTo("annotationsTest/whatever", "");
|
||||
|
||||
// test fails if TypedFilter ignores @StaplerAccessibleType
|
||||
j.createWebClient().goTo("annotationsTest/transit/response", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.TestExtension;
|
||||
import org.kohsuke.stapler.Ancestor;
|
||||
import org.kohsuke.stapler.HttpResponses;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
import org.kohsuke.stapler.StaplerProxy;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
|
||||
public class PreventRoutingTest extends StaplerAbstractTest {
|
||||
|
||||
@TestExtension
|
||||
public static class TargetNull extends AbstractUnprotectedRootAction implements StaplerProxy {
|
||||
@Override
|
||||
public @CheckForNull String getUrlName() {
|
||||
return "target-null";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getTarget() {
|
||||
// in case of null, it's "this" that is considered
|
||||
return null;
|
||||
}
|
||||
|
||||
public Renderable getLegitRoutable(){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
// TODO un-ignore once we use a Stapler release with the fix for this
|
||||
@Ignore("Does not behave as intended before https://github.com/stapler/stapler/pull/149")
|
||||
public void getTargetNull_isNotRoutable() throws Exception {
|
||||
assertNotReachable("target-null/legitRoutable");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class TargetNewObject extends AbstractUnprotectedRootAction implements StaplerProxy {
|
||||
@Override
|
||||
public @CheckForNull String getUrlName() {
|
||||
return "target-new-object";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getTarget() {
|
||||
// Object is not routable
|
||||
return new Object();
|
||||
}
|
||||
|
||||
public Renderable getLegitRoutable(){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getTargetNewObject_isNotRoutable() throws Exception {
|
||||
assertNotReachable("target-new-object/legitRoutable");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class NotARequest extends AbstractUnprotectedRootAction {
|
||||
@Override
|
||||
public @CheckForNull String getUrlName() {
|
||||
return "not-a-request";
|
||||
}
|
||||
|
||||
public Renderable getLegitRoutable(){
|
||||
notStaplerGetter(this);
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
// just to validate it's ok
|
||||
public Renderable getLegitRoutable2(){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
|
||||
private static void notStaplerGetter(@Nonnull Object o){
|
||||
StaplerRequest req = Stapler.getCurrentRequest();
|
||||
if (req != null) {
|
||||
List<Ancestor> ancestors = req.getAncestors();
|
||||
if (!ancestors.isEmpty() && ancestors.get(ancestors.size() - 1).getObject() == o) {
|
||||
throw HttpResponses.notFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regularGetter_notARequest() throws Exception {
|
||||
assertReachable("not-a-request/legitRoutable2");
|
||||
assertNotReachable("not-a-request/legitRoutable");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import com.cloudbees.hudson.plugins.folder.computed.FolderCron;
|
||||
import com.gargoylesoftware.htmlunit.HttpMethod;
|
||||
import com.gargoylesoftware.htmlunit.Page;
|
||||
import com.gargoylesoftware.htmlunit.WebRequest;
|
||||
import hudson.Launcher;
|
||||
import hudson.model.AbstractBuild;
|
||||
import hudson.model.AsyncPeriodicWork;
|
||||
import hudson.model.BuildListener;
|
||||
import hudson.model.Descriptor;
|
||||
import hudson.model.FreeStyleBuild;
|
||||
import hudson.model.FreeStyleProject;
|
||||
import hudson.model.PeriodicWork;
|
||||
import hudson.model.Result;
|
||||
import hudson.model.TaskListener;
|
||||
import hudson.model.queue.QueueTaskFuture;
|
||||
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
|
||||
import hudson.security.HudsonPrivateSecurityRealm;
|
||||
import hudson.tasks.Builder;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.junit.After;
|
||||
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 org.jvnet.hudson.test.TestExtension;
|
||||
import org.kohsuke.stapler.WebApp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* To check the previous behavior you can use:
|
||||
* <pre>
|
||||
* {@link org.kohsuke.stapler.MetaClass#LEGACY_WEB_METHOD_MODE} = true;
|
||||
* {@link org.kohsuke.stapler.MetaClass#LEGACY_GETTER_MODE} = true;
|
||||
* </pre>
|
||||
*/
|
||||
@Issue("SECURITY-400")
|
||||
public class Security400Test {
|
||||
@Rule
|
||||
public JenkinsRule j = new JenkinsRule();
|
||||
|
||||
private static boolean filteredDoActionTriggered = false;
|
||||
|
||||
@Before
|
||||
public void prepareFilterListener(){
|
||||
WebApp webApp = WebApp.get(j.jenkins.servletContext);
|
||||
webApp.setFilteredDoActionTriggerListener((f, req, rsp, node) -> {
|
||||
filteredDoActionTriggered = true;
|
||||
return false;
|
||||
});
|
||||
webApp.setFilteredGetterTriggerListener((f, req, rsp, node, expression) -> {
|
||||
filteredDoActionTriggered = true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@After
|
||||
public void resetFilter(){
|
||||
filteredDoActionTriggered = false;
|
||||
}
|
||||
|
||||
private void assertRequestWasBlockedAndResetFlag(){
|
||||
assertTrue("No request was blocked", filteredDoActionTriggered);
|
||||
filteredDoActionTriggered = false;
|
||||
}
|
||||
|
||||
private void assertRequestWasNotBlocked(){
|
||||
assertFalse("There was at least a request that was blocked", filteredDoActionTriggered);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-391")
|
||||
public void asyncDoRun() throws Exception {
|
||||
j.createWebClient().assertFails("extensionList/" + AsyncPeriodicWork.class.getName() + "/" + Work.class.getName() + "/run", HttpURLConnection.HTTP_NOT_FOUND);
|
||||
Thread.sleep(1000); // give the thread a moment to finish
|
||||
assertFalse("should never have run", ran);
|
||||
}
|
||||
|
||||
private static boolean ran;
|
||||
|
||||
@TestExtension("asyncDoRun")
|
||||
public static class Work extends AsyncPeriodicWork {
|
||||
public Work() {
|
||||
super("Test");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRecurrencePeriod() {
|
||||
return Long.MAX_VALUE; // do not run after init()
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(TaskListener listener) throws IOException, InterruptedException {
|
||||
ran = true;
|
||||
}
|
||||
}
|
||||
|
||||
// require a dependency on cloudbees-folder-plugin
|
||||
@Test
|
||||
@Issue("SECURITY-397")
|
||||
// particular case of SECURITY-391
|
||||
public void folderCronDoRun() throws Exception {
|
||||
j.createWebClient().assertFails("extensionList/" + PeriodicWork.class.getName() + "/" + FolderCron.class.getName() + "/run", HttpURLConnection.HTTP_NOT_FOUND);
|
||||
assertRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
/**
|
||||
* replacement of "computers/0/executors/0/contextClassLoader/context/handlers/0/sessionManager/stop" attack
|
||||
*/
|
||||
@Test
|
||||
@Issue("SECURITY-404")
|
||||
public void avoidDangerousAccessToSession() throws Exception {
|
||||
j.jenkins.setCrumbIssuer(null);
|
||||
|
||||
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
|
||||
j.jenkins.setAuthorizationStrategy(
|
||||
new MockAuthorizationStrategy()
|
||||
.grant(Jenkins.ADMINISTER).everywhere().to("admin")
|
||||
.grant(Jenkins.READ).everywhere().to("user")
|
||||
);
|
||||
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
wc.login("admin");
|
||||
|
||||
JenkinsRule.WebClient wc2 = j.createWebClient();
|
||||
wc2.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
wc2.login("user");
|
||||
|
||||
Page page;
|
||||
|
||||
page = wc.goTo("whoAmI/api/xml/", null);
|
||||
System.out.println(page.getWebResponse().getContentAsString());
|
||||
assertThat(page.getWebResponse().getContentAsString(), containsString("<anonymous>false</anonymous>"));
|
||||
|
||||
page = wc2.goTo("whoAmI/api/xml/", null);
|
||||
System.out.println(page.getWebResponse().getContentAsString());
|
||||
assertThat(page.getWebResponse().getContentAsString(), containsString("<anonymous>false</anonymous>"));
|
||||
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
// the doXxx fix prevents the doStop to be executed
|
||||
// and in addition the getXxx fix prevents the getContextHandler to be used as navigation
|
||||
|
||||
// the first beans/0 return the HashedSession
|
||||
// the second beans/0 return the HashSessionManager
|
||||
page = wc2.goTo("adjuncts/<randomString>/webApp/context/contextHandler/beans/0/beans/0/stop", null);
|
||||
// other possible path
|
||||
// page = wc.goTo("adjuncts/<randomString>/webApp/someStapler/currentRequest/session/servletContext/contextHandler/beans/0/beans/0/stop", null);
|
||||
// page = wc.goTo("adjuncts/<randomString>/webApp/someStapler/currentRequest/servletContext/contextHandler/beans/0/beans/0/stop", null);
|
||||
|
||||
// assertEquals(404, page.getWebResponse().getStatusCode());
|
||||
// assertRequestWasBlockedAndResetFlag();
|
||||
// getWebApp is now forbidden
|
||||
assertEquals(403, page.getWebResponse().getStatusCode());
|
||||
|
||||
// if the call was successful, both are disconnected and anonymous would have been true
|
||||
|
||||
page = wc.goTo("whoAmI/api/xml/", null);
|
||||
System.out.println(page.getWebResponse().getContentAsString());
|
||||
assertThat(page.getWebResponse().getContentAsString(), containsString("<anonymous>false</anonymous>"));
|
||||
|
||||
page = wc2.goTo("whoAmI/api/xml/", null);
|
||||
System.out.println(page.getWebResponse().getContentAsString());
|
||||
assertThat(page.getWebResponse().getContentAsString(), containsString("<anonymous>false</anonymous>"));
|
||||
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
// similar approach but different impact:
|
||||
// can put null into desired session key (no impact yet)
|
||||
// session impl. is HashedSession
|
||||
// page = wc.goTo("adjuncts/<randomString>/webApp/someStapler/currentRequest/session/putOrRemove/ACEGI_SECURITY_CONTEXT/", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-404")
|
||||
public void ensureDoStopStillReachable() throws Exception {
|
||||
j.jenkins.setCrumbIssuer(null);
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
// used as a reference passed to the build step
|
||||
AtomicInteger atomicResult = new AtomicInteger(0);
|
||||
FreeStyleProject p = j.createFreeStyleProject();
|
||||
|
||||
final Semaphore semaphore = new Semaphore(0);
|
||||
|
||||
p.getBuildersList().add(new SemaphoredBuilder(semaphore, atomicResult));
|
||||
|
||||
// to be sure to reach the correct one
|
||||
j.jenkins.setNumExecutors(1);
|
||||
|
||||
{ // preliminary test, calling the stop method without any executor results in 404
|
||||
WebRequest request = new WebRequest(new URL(j.getURL() + "computers/0/executors/0/stop"), HttpMethod.POST);
|
||||
Page page = wc.getPage(request);
|
||||
assertEquals(404, page.getWebResponse().getStatusCode());
|
||||
assertRequestWasNotBlocked();
|
||||
}
|
||||
|
||||
{ // first try, we let the build finishes normally
|
||||
QueueTaskFuture<FreeStyleBuild> futureBuild = p.scheduleBuild2(0);
|
||||
futureBuild.waitForStart();
|
||||
|
||||
// let the build finishes
|
||||
semaphore.release(1);
|
||||
j.assertBuildStatus(Result.SUCCESS, futureBuild);
|
||||
assertEquals(1, atomicResult.get());
|
||||
}
|
||||
|
||||
{ // second try, we need to reach the stop method in executor to interrupt the build
|
||||
atomicResult.set(0);
|
||||
assertEquals(0, atomicResult.get());
|
||||
QueueTaskFuture<FreeStyleBuild> futureBuild = p.scheduleBuild2(0);
|
||||
futureBuild.waitForStart();
|
||||
|
||||
WebRequest request = new WebRequest(new URL(j.getURL() + "computers/0/executors/0/stop"), HttpMethod.POST);
|
||||
Page page = wc.getPage(request);
|
||||
assertEquals(404, page.getWebResponse().getStatusCode());
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
j.assertBuildStatus(Result.FAILURE, futureBuild);
|
||||
assertEquals(3, atomicResult.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-404")
|
||||
public void anonCannotReadTextConsole() throws Exception {
|
||||
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
|
||||
FullControlOnceLoggedInAuthorizationStrategy authorizationStrategy = new FullControlOnceLoggedInAuthorizationStrategy();
|
||||
authorizationStrategy.setAllowAnonymousRead(false);
|
||||
j.jenkins.setAuthorizationStrategy(authorizationStrategy);
|
||||
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
FreeStyleProject p = j.createFreeStyleProject();
|
||||
|
||||
Semaphore semaphore = new Semaphore(0);
|
||||
|
||||
p.getBuildersList().add(new SemaphoredBuilder(semaphore, new AtomicInteger(0)));
|
||||
|
||||
// to be sure to reach the correct one
|
||||
j.jenkins.setNumExecutors(1);
|
||||
|
||||
{ // preliminary test, calling the consoleText method without any executor results in 404
|
||||
Page page = wc.goTo("computers/0/executors/0/currentExecutable/consoleText", null);
|
||||
checkPageIsRedirectedToLogin(page);
|
||||
assertRequestWasNotBlocked();
|
||||
}
|
||||
|
||||
{ // as Connected User, we start the build and try to get the console, ensure current expected behavior still works
|
||||
wc.login("foo");
|
||||
|
||||
QueueTaskFuture<FreeStyleBuild> futureBuild = p.scheduleBuild2(0);
|
||||
futureBuild.waitForStart();
|
||||
|
||||
Page page = wc.goTo("computers/0/executors/0/currentExecutable/consoleText", null);
|
||||
assertEquals(200, page.getWebResponse().getStatusCode());
|
||||
assertThat(page.getWebResponse().getContentAsString(), containsString(SemaphoredBuilder.START_MESSAGE));
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
semaphore.release(1);
|
||||
j.assertBuildStatus(Result.SUCCESS, futureBuild);
|
||||
}
|
||||
|
||||
{ // as Anonymous, we start the build and try to get the console
|
||||
wc = j.createWebClient();
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
QueueTaskFuture<FreeStyleBuild> futureBuild = p.scheduleBuild2(0);
|
||||
futureBuild.waitForStart();
|
||||
|
||||
Page page = wc.goTo("computers/0/executors/0/currentExecutable/consoleText", null);
|
||||
checkPageIsRedirectedToLogin(page);
|
||||
assertThat(page.getWebResponse().getContentAsString(), not(containsString(SemaphoredBuilder.START_MESSAGE)));
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
semaphore.release(1);
|
||||
j.assertBuildStatus(Result.SUCCESS, futureBuild);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-404")
|
||||
public void anonCannotAccessExecutorApi() throws Exception {
|
||||
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
|
||||
FullControlOnceLoggedInAuthorizationStrategy authorizationStrategy = new FullControlOnceLoggedInAuthorizationStrategy();
|
||||
authorizationStrategy.setAllowAnonymousRead(false);
|
||||
j.jenkins.setAuthorizationStrategy(authorizationStrategy);
|
||||
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
FreeStyleProject p = j.createFreeStyleProject();
|
||||
|
||||
Semaphore semaphore = new Semaphore(0);
|
||||
|
||||
p.getBuildersList().add(new SemaphoredBuilder(semaphore, new AtomicInteger(0)));
|
||||
|
||||
// to be sure to reach the correct one
|
||||
j.jenkins.setNumExecutors(1);
|
||||
|
||||
{
|
||||
Page page = wc.goTo("computers/0/executors/0/api/xml", null);
|
||||
checkPageIsRedirectedToLogin(page);
|
||||
assertRequestWasNotBlocked();
|
||||
}
|
||||
|
||||
{ // as Connected User, we start the build and can access the executor api
|
||||
QueueTaskFuture<FreeStyleBuild> futureBuild = p.scheduleBuild2(0);
|
||||
futureBuild.waitForStart();
|
||||
|
||||
wc.login("foo");
|
||||
Page page = wc.goTo("computers/0/executors/0/api/xml", null);
|
||||
assertEquals(200, page.getWebResponse().getStatusCode());
|
||||
assertThat(page.getWebResponse().getContentAsString(), containsString(p.getUrl()));
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
semaphore.release(1);
|
||||
j.assertBuildStatus(Result.SUCCESS, futureBuild);
|
||||
|
||||
wc = j.createWebClient();
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
}
|
||||
|
||||
{ // as Anonymous, we start the build and cannot access the executor api
|
||||
QueueTaskFuture<FreeStyleBuild> futureBuild = p.scheduleBuild2(0);
|
||||
futureBuild.waitForStart();
|
||||
|
||||
Page page = wc.goTo("computers/0/executors/0/api/xml", null);
|
||||
checkPageIsRedirectedToLogin(page);
|
||||
assertThat(page.getWebResponse().getContentAsString(), not(containsString(p.getUrl())));
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
semaphore.release(1);
|
||||
j.assertBuildStatus(Result.SUCCESS, futureBuild);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-404")
|
||||
public void anonCannotAccessJenkinsItemMap() throws Exception {
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
FreeStyleProject p = j.createFreeStyleProject();
|
||||
|
||||
{ // try to access /itemMap/<jobName>
|
||||
wc.login("foo");
|
||||
Page page = wc.goTo("itemMap/" + p.getName() + "/api/xml", null);
|
||||
assertEquals(404, page.getWebResponse().getStatusCode());
|
||||
assertThat(page.getWebResponse().getContentAsString(), not(containsString("<freeStyleProject")));
|
||||
assertRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SemaphoredBuilder extends Builder {
|
||||
private static final String START_MESSAGE = "job started, will try to acquire one permit";
|
||||
private transient Semaphore semaphore;
|
||||
private transient AtomicInteger atomicInteger;
|
||||
|
||||
SemaphoredBuilder(Semaphore semaphore, AtomicInteger atomicInteger) {
|
||||
this.semaphore = semaphore;
|
||||
this.atomicInteger = atomicInteger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
|
||||
try {
|
||||
listener.getLogger().println(START_MESSAGE);
|
||||
boolean result = semaphore.tryAcquire(20, TimeUnit.SECONDS);
|
||||
if (result) {
|
||||
listener.getLogger().println("permit acquired");
|
||||
atomicInteger.set(1);
|
||||
return true;
|
||||
} else {
|
||||
atomicInteger.set(2);
|
||||
return false;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
atomicInteger.set(3);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class DescriptorImpl extends Descriptor<Builder> {}
|
||||
}
|
||||
|
||||
// currently there is no other way to reach logRecorderManager in core / or plugin
|
||||
@Test
|
||||
@Issue("SECURITY-471")
|
||||
public void ensureLogRecordManagerAccessibleOnlyByAdmin() throws Exception {
|
||||
j.jenkins.setCrumbIssuer(null);
|
||||
|
||||
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
|
||||
j.jenkins.setAuthorizationStrategy(
|
||||
new MockAuthorizationStrategy()
|
||||
.grant(Jenkins.ADMINISTER).everywhere().to("admin")
|
||||
.grant(Jenkins.READ).everywhere().to("user")
|
||||
);
|
||||
|
||||
String logNameForAdmin = "testLoggerAdmin";
|
||||
String logNameForUser = "testLoggerUser";
|
||||
|
||||
{ // admin can do everything
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.login("admin");
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
// ensure the logger does not exist before the creation
|
||||
assertEquals(404, wc.goTo("log/" + logNameForAdmin + "/autoCompleteLoggerName/?value=a", null).getWebResponse().getStatusCode());
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
WebRequest request = new WebRequest(new URL(j.getURL() + "log/newLogRecorder/?name=" + logNameForAdmin), HttpMethod.POST);
|
||||
|
||||
wc.getOptions().setRedirectEnabled(false);
|
||||
Page page = wc.getPage(request);
|
||||
assertEquals(302, page.getWebResponse().getStatusCode());
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
// after creation the logger exists
|
||||
j.assertGoodStatus(wc.goTo("log/" + logNameForAdmin + "/autoCompleteLoggerName/?value=a", null));
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
assertEquals(404, wc.goTo("log/" + "nonExistingName" + "/autoCompleteLoggerName/?value=a", null).getWebResponse().getStatusCode());
|
||||
assertRequestWasNotBlocked();
|
||||
}
|
||||
|
||||
{ // user is blocked
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.login("user");
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
// no right to check the existence of a logger
|
||||
assertEquals(403, wc.goTo("log/" + logNameForUser + "/autoCompleteLoggerName/?value=a", null).getWebResponse().getStatusCode());
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
WebRequest request = new WebRequest(new URL(j.getURL() + "log/newLogRecorder/?name=" + logNameForUser), HttpMethod.POST);
|
||||
|
||||
wc.getOptions().setRedirectEnabled(false);
|
||||
Page page = wc.getPage(request);
|
||||
assertEquals(403, page.getWebResponse().getStatusCode());
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
// after the failed attempt, the logger is not created
|
||||
assertEquals(403, wc.goTo("log/" + logNameForUser + "/autoCompleteLoggerName/?value=a", null).getWebResponse().getStatusCode());
|
||||
assertRequestWasNotBlocked();
|
||||
}
|
||||
|
||||
{ // admin can check the non-existence after user failed creation also
|
||||
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.login("admin");
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
// ensure the logger was not created by the user (check in case the request returned 403 but created the logger silently)
|
||||
assertEquals(404, wc.goTo("log/" + logNameForUser + "/autoCompleteLoggerName/?value=a", null).getWebResponse().getStatusCode());
|
||||
assertRequestWasNotBlocked();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonCannotHaveTheListOfUsers() throws Exception {
|
||||
j.jenkins.setCrumbIssuer(null);
|
||||
|
||||
FullControlOnceLoggedInAuthorizationStrategy authorizationStrategy = new FullControlOnceLoggedInAuthorizationStrategy();
|
||||
j.jenkins.setAuthorizationStrategy(authorizationStrategy);
|
||||
|
||||
HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
j.jenkins.setSecurityRealm(securityRealm);
|
||||
securityRealm.createAccount("admin", "admin");
|
||||
securityRealm.createAccount("secretUser", "secretUser");
|
||||
|
||||
{ // admin should have access to the user list
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.login("admin");
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
Page page = wc.goTo("securityRealm");
|
||||
assertEquals(200, page.getWebResponse().getStatusCode());
|
||||
assertThat(page.getWebResponse().getContentAsString(), containsString("secretUser"));
|
||||
assertRequestWasNotBlocked();
|
||||
}
|
||||
|
||||
// with or without the anonymousRead, anonymous are not allowed to have access to
|
||||
// list of users in securityRealm
|
||||
authorizationStrategy.setAllowAnonymousRead(true);
|
||||
{ // without any read permission the anon have access to the user list
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
wc.getOptions().setRedirectEnabled(false);
|
||||
|
||||
Page page = wc.goTo("securityRealm/", null);
|
||||
checkPageIsRedirectedToLogin(page);
|
||||
assertThat(page.getWebResponse().getContentAsString(), not(containsString("secretUser")));
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
page = wc.goTo("asynchPeople/", null);
|
||||
assertEquals(200, page.getWebResponse().getStatusCode());
|
||||
// javascript will load the user list asynch
|
||||
assertThat(page.getWebResponse().getContentAsString(), containsString("Includes all known"));
|
||||
assertRequestWasNotBlocked();
|
||||
}
|
||||
|
||||
authorizationStrategy.setAllowAnonymousRead(false);
|
||||
{ // and with restriction, the anonymous users cannot read the user list
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
wc.getOptions().setRedirectEnabled(false);
|
||||
|
||||
Page page = wc.goTo("securityRealm/", null);
|
||||
checkPageIsRedirectedToLogin(page);
|
||||
assertThat(page.getWebResponse().getContentAsString(), not(containsString("secretUser")));
|
||||
assertRequestWasNotBlocked();
|
||||
|
||||
// with the restriction we disallow the anon to even read the list of all users
|
||||
page = wc.goTo("asynchPeople/", null);
|
||||
checkPageIsRedirectedToLogin(page);
|
||||
assertThat(page.getWebResponse().getContentAsString(), not(containsString("secretUser")));
|
||||
assertRequestWasNotBlocked();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-722")
|
||||
public void noAccessToAllUsers() throws Exception {
|
||||
j.jenkins.setCrumbIssuer(null);
|
||||
HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
j.jenkins.setSecurityRealm(securityRealm);
|
||||
securityRealm.createAccount("admin", "admin");
|
||||
|
||||
j.jenkins.setAuthorizationStrategy(
|
||||
new MockAuthorizationStrategy()
|
||||
.grant(Jenkins.ADMINISTER).everywhere().to("admin")
|
||||
);
|
||||
|
||||
{ // neither anon have access to the allUsers end point
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
// anonymous user
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
Page page = wc.goTo("securityRealm/allUsers/" + 0 + "/descriptorByName/jenkins.security.ApiTokenProperty/help/apiToken/");
|
||||
assertEquals(404, page.getWebResponse().getStatusCode());
|
||||
assertRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
{ // nor the admin have that access
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.login("admin");
|
||||
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
Page page = wc.goTo("securityRealm/allUsers/" + 0 + "/descriptorByName/jenkins.security.ApiTokenProperty/help/apiToken/");
|
||||
assertEquals(404, page.getWebResponse().getStatusCode());
|
||||
assertRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
}
|
||||
|
||||
// // does not work in 2.60 since the method was added in 2.91+
|
||||
// String newLogin = "newUser";
|
||||
// j.createWebClient().goTo("securityRealm/allUsers/0/orCreateByIdOrFullName/" + newLogin + "/");
|
||||
|
||||
private void checkPageIsRedirectedToLogin(Page page) {
|
||||
assertEquals(200, page.getWebResponse().getStatusCode());
|
||||
assertThat(page.getUrl().getPath(), containsString("login"));
|
||||
assertThat(page.getUrl().getQuery(), containsString("from"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
|
||||
import com.gargoylesoftware.htmlunit.HttpMethod;
|
||||
import com.gargoylesoftware.htmlunit.Page;
|
||||
import com.gargoylesoftware.htmlunit.WebRequest;
|
||||
import hudson.model.UnprotectedRootAction;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
import org.kohsuke.stapler.StaplerResponse;
|
||||
import org.kohsuke.stapler.WebApp;
|
||||
import org.kohsuke.stapler.WebMethod;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.startsWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public abstract class StaplerAbstractTest {
|
||||
@ClassRule
|
||||
public static JenkinsRule rule = new JenkinsRule();
|
||||
protected JenkinsRule j;
|
||||
|
||||
protected JenkinsRule.WebClient wc;
|
||||
|
||||
protected WebApp webApp;
|
||||
|
||||
protected static boolean filteredGetMethodTriggered = false;
|
||||
protected static boolean filteredDoActionTriggered = false;
|
||||
protected static boolean filteredFieldTriggered = false;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
j = rule;
|
||||
j.jenkins.setCrumbIssuer(null);
|
||||
wc = j.createWebClient();
|
||||
|
||||
this.webApp = (WebApp) j.jenkins.servletContext.getAttribute(WebApp.class.getName());
|
||||
|
||||
webApp.setFilteredGetterTriggerListener((f, req, rst, node, expression) -> {
|
||||
filteredGetMethodTriggered = true;
|
||||
return false;
|
||||
});
|
||||
webApp.setFilteredDoActionTriggerListener((f, req, rsp, node) -> {
|
||||
filteredDoActionTriggered = true;
|
||||
return false;
|
||||
});
|
||||
webApp.setFilteredFieldTriggerListener((f, req, rsp, node, expression) -> {
|
||||
filteredFieldTriggered = true;
|
||||
return false;
|
||||
});
|
||||
|
||||
filteredGetMethodTriggered = false;
|
||||
filteredDoActionTriggered = false;
|
||||
filteredFieldTriggered = false;
|
||||
}
|
||||
|
||||
//================================= utility class =================================
|
||||
|
||||
protected static class AbstractUnprotectedRootAction implements UnprotectedRootAction {
|
||||
@Override
|
||||
public @CheckForNull String getIconFileName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @CheckForNull String getDisplayName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @CheckForNull String getUrlName() {
|
||||
return StringUtils.uncapitalize(this.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
public static final String RENDERABLE_CLASS_SIGNATURE = "class jenkins.security.stapler.StaplerAbstractTest.Renderable";
|
||||
protected static class Renderable {
|
||||
|
||||
public void doIndex() {replyOk();}
|
||||
|
||||
@WebMethod(name = "valid")
|
||||
public void valid() {replyOk();}
|
||||
}
|
||||
|
||||
protected static class ParentRenderable {
|
||||
public Renderable getRenderable(){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
|
||||
protected static class RenderablePoint extends Point {
|
||||
public void doIndex() {replyOk();}
|
||||
}
|
||||
|
||||
//================================= utility methods =================================
|
||||
|
||||
protected static void replyOk() {
|
||||
StaplerResponse resp = Stapler.getCurrentResponse();
|
||||
try {
|
||||
resp.getWriter().write("ok");
|
||||
resp.flushBuffer();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
|
||||
//================================= testing methods =================================
|
||||
|
||||
protected void assertGetMethodRequestWasBlockedAndResetFlag() {
|
||||
assertTrue("No get method request was blocked", filteredGetMethodTriggered);
|
||||
filteredGetMethodTriggered = false;
|
||||
}
|
||||
protected void assertDoActionRequestWasBlockedAndResetFlag() {
|
||||
assertTrue("No do action request was blocked", filteredDoActionTriggered);
|
||||
filteredDoActionTriggered = false;
|
||||
}
|
||||
protected void assertFieldRequestWasBlockedAndResetFlag() {
|
||||
assertTrue("No field request was blocked", filteredFieldTriggered);
|
||||
filteredFieldTriggered = false;
|
||||
}
|
||||
protected void assertGetMethodActionRequestWasNotBlocked() {
|
||||
assertFalse("There was at least one get method request that was blocked", filteredGetMethodTriggered);
|
||||
}
|
||||
protected void assertDoActionRequestWasNotBlocked() {
|
||||
assertFalse("There was at least one do action request that was blocked", filteredDoActionTriggered);
|
||||
}
|
||||
protected void assertFieldRequestWasNotBlocked() {
|
||||
assertFalse("There was at least one field request that was blocked", filteredFieldTriggered);
|
||||
}
|
||||
|
||||
protected void assertReachable(String url, HttpMethod method) throws IOException {
|
||||
try {
|
||||
Page page = wc.getPage(new WebRequest(new URL(j.getURL(), url), method));
|
||||
assertEquals(200, page.getWebResponse().getStatusCode());
|
||||
assertThat(page.getWebResponse().getContentAsString(), startsWith("ok"));
|
||||
|
||||
assertDoActionRequestWasNotBlocked();
|
||||
assertGetMethodActionRequestWasNotBlocked();
|
||||
assertFieldRequestWasNotBlocked();
|
||||
} catch (FailingHttpStatusCodeException e) {
|
||||
fail("Url " + url + " should be reachable, received " + e.getMessage() + " (" + e.getStatusCode() + ") instead.");
|
||||
}
|
||||
}
|
||||
|
||||
protected void assertReachable(String url) throws IOException {
|
||||
assertReachable(url, HttpMethod.GET);
|
||||
}
|
||||
|
||||
protected void assertReachableWithSettings(WebRequest request) throws IOException {
|
||||
Page page = wc.getPage(request);
|
||||
assertEquals(200, page.getWebResponse().getStatusCode());
|
||||
assertEquals("ok", page.getWebResponse().getContentAsString());
|
||||
assertDoActionRequestWasNotBlocked();
|
||||
}
|
||||
|
||||
protected void assertReachableWithoutOk(String url) throws IOException {
|
||||
try {
|
||||
Page page = wc.getPage(new URL(j.getURL(), url));
|
||||
assertEquals(200, page.getWebResponse().getStatusCode());
|
||||
} catch (FailingHttpStatusCodeException e) {
|
||||
fail("Url " + url + " should be reachable, received " + e.getMessage() + " (" + e.getStatusCode() + ") instead.");
|
||||
}
|
||||
}
|
||||
|
||||
protected void assertNotReachable(String url) throws IOException {
|
||||
try {
|
||||
wc.getPage(new URL(j.getURL(), url));
|
||||
fail("Url " + url + " is reachable but should not be, an not-found error is expected");
|
||||
} catch (FailingHttpStatusCodeException e) {
|
||||
assertEquals("Url " + url + " returns an error different from 404", 404, e.getResponse().getStatusCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.For;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.TestExtension;
|
||||
import org.kohsuke.stapler.HttpResponses;
|
||||
import org.kohsuke.stapler.QueryParameter;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.kohsuke.stapler.WebMethod;
|
||||
|
||||
@Issue("SECURITY-400")
|
||||
@For({StaplerDispatchable.class, StaplerNotDispatchable.class, DoActionFilter.class})
|
||||
public class StaplerRoutableActionTest extends StaplerAbstractTest {
|
||||
|
||||
@TestExtension
|
||||
public static class TestNewRulesRoutableAction extends AbstractUnprotectedRootAction {
|
||||
// StaplerDispatchable is not enough, the method needs to have at least either a name starting with do* or a WebMethod annotation
|
||||
@StaplerDispatchable
|
||||
public void notDoName() { replyOk(); }
|
||||
|
||||
@StaplerDispatchable // could be used to indicate that's a web method, without having to use @WebMethod
|
||||
public void doWebMethod1() { replyOk(); }
|
||||
|
||||
// without annotation, returnType, parameter, exception => not a web method
|
||||
public void doWebMethod2() { replyOk(); }
|
||||
|
||||
public void doWebMethod3() throws HttpResponses.HttpResponseException {
|
||||
replyOk();
|
||||
}
|
||||
|
||||
public void doWebMethod4(StaplerRequest request) {
|
||||
replyOk();
|
||||
}
|
||||
|
||||
public void doWebMethod5(@QueryParameter String foo) {
|
||||
replyOk();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNewRulesRoutableAction_notDoName() throws Exception {
|
||||
assertNotReachable("testNewRulesRoutableAction/notDoName/");
|
||||
// not even considered as a blocked action because the filter is not even called, they are lacking do* or @WebMethod
|
||||
// assertDoActionRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("testNewRulesRoutableAction/tDoName/");
|
||||
// assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNewRulesRoutableAction_webMethod1() throws Exception {
|
||||
assertReachable("testNewRulesRoutableAction/webMethod1/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNewRulesRoutableAction_webMethod3Through5() throws Exception {
|
||||
assertReachable("testNewRulesRoutableAction/webMethod3/");
|
||||
assertReachable("testNewRulesRoutableAction/webMethod4/");
|
||||
assertReachable("testNewRulesRoutableAction/webMethod5/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNewRulesRoutableAction_webMethod2() throws Exception {
|
||||
assertNotReachable("testNewRulesRoutableAction/webMethod2/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class TestNewRulesNonroutableAction extends AbstractUnprotectedRootAction {
|
||||
@StaplerNotDispatchable
|
||||
public void doWebMethod1() { replyOk(); }
|
||||
|
||||
@StaplerNotDispatchable
|
||||
@WebMethod(name = "webMethod2")
|
||||
public void doWebMethod2() { replyOk(); }
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNewRulesNonroutableAction_webMethod1() throws Exception {
|
||||
assertNotReachable("testNewRulesNonroutableAction/webMethod1/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNewRulesNonroutableAction_webMethod2() throws Exception {
|
||||
// priority of negative over positive
|
||||
assertNotReachable("testNewRulesNonroutableAction/webMethod2/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.For;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.TestExtension;
|
||||
|
||||
@Issue("SECURITY-595")
|
||||
@For({StaplerDispatchable.class, StaplerNotDispatchable.class, TypedFilter.class})
|
||||
public class StaplerRoutableFieldTest extends StaplerAbstractTest {
|
||||
@TestExtension
|
||||
public static class TestRootAction extends AbstractUnprotectedRootAction {
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return "test";
|
||||
}
|
||||
|
||||
public Renderable renderableNotAnnotated = new Renderable();
|
||||
|
||||
public ParentRenderable parentRenderableNotAnnotated = new ParentRenderable();
|
||||
|
||||
public Object objectNotAnnotated = new Renderable();
|
||||
|
||||
@StaplerDispatchable
|
||||
public Renderable renderableAnnotatedOk = new Renderable();
|
||||
|
||||
@StaplerDispatchable
|
||||
public ParentRenderable parentRenderableAnnotatedOk = new ParentRenderable();
|
||||
|
||||
@StaplerDispatchable
|
||||
public Object objectAnnotatedOk = new Renderable();
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public Renderable renderableAnnotatedKo = new Renderable();
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public Object objectAnnotatedKo = new Renderable();
|
||||
|
||||
@StaplerDispatchable
|
||||
@StaplerNotDispatchable
|
||||
public Renderable renderableDoubleAnnotated = new Renderable();
|
||||
|
||||
@StaplerDispatchable
|
||||
@StaplerNotDispatchable
|
||||
public Object objectDoubleAnnotated = new Renderable();
|
||||
|
||||
public static Renderable staticRenderableNotAnnotated = new Renderable();
|
||||
|
||||
public static Object staticObjectNotAnnotated = new Renderable();
|
||||
|
||||
@StaplerDispatchable
|
||||
public static Renderable staticRenderableAnnotatedOk = new Renderable();
|
||||
|
||||
@StaplerDispatchable
|
||||
public static Object staticObjectAnnotatedOk = new Renderable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldNotAnnotated() throws Exception {
|
||||
assertReachable("test/renderableNotAnnotated/");
|
||||
assertReachable("test/renderableNotAnnotated/valid/");
|
||||
|
||||
assertNotReachable("test/parentRenderableNotAnnotated/");
|
||||
assertNotReachable("test/parentRenderableNotAnnotated/renderable/");
|
||||
assertNotReachable("test/parentRenderableNotAnnotated/renderable/valid/");
|
||||
|
||||
assertNotReachable("test/objectNotAnnotated/");
|
||||
assertNotReachable("test/objectNotAnnotated/valid/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldNotAnnotated_escapeHatch() throws Exception {
|
||||
boolean currentValue = TypedFilter.SKIP_TYPE_CHECK;
|
||||
try {
|
||||
TypedFilter.SKIP_TYPE_CHECK = true;
|
||||
// to apply the new configuration
|
||||
webApp.clearMetaClassCache();
|
||||
|
||||
assertReachable("test/renderableNotAnnotated/");
|
||||
assertReachable("test/renderableNotAnnotated/valid/");
|
||||
|
||||
assertNotReachable("test/parentRenderableNotAnnotated/");
|
||||
assertReachable("test/parentRenderableNotAnnotated/renderable/");
|
||||
assertReachable("test/parentRenderableNotAnnotated/renderable/valid/");
|
||||
} finally {
|
||||
TypedFilter.SKIP_TYPE_CHECK = currentValue;
|
||||
// to reset the configuration
|
||||
webApp.clearMetaClassCache();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldAnnotatedOk() throws Exception {
|
||||
assertReachable("test/renderableAnnotatedOk/");
|
||||
assertReachable("test/renderableAnnotatedOk/valid/");
|
||||
|
||||
assertReachable("test/objectAnnotatedOk/");
|
||||
assertReachable("test/objectAnnotatedOk/valid/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldAnnotatedKo() throws Exception {
|
||||
assertNotReachable("test/renderableAnnotatedKo/");
|
||||
assertNotReachable("test/renderableAnnotatedKo/valid/");
|
||||
|
||||
assertNotReachable("test/objectAnnotatedKo/");
|
||||
assertNotReachable("test/objectAnnotatedKo/valid/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldDoubleAnnotated() throws Exception {
|
||||
assertNotReachable("test/renderableDoubleAnnotated/");
|
||||
assertNotReachable("test/renderableDoubleAnnotated/valid/");
|
||||
|
||||
assertNotReachable("test/objectDoubleAnnotated/");
|
||||
assertNotReachable("test/objectDoubleAnnotated/valid/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStaticFieldNotAnnotated() throws Exception {
|
||||
assertNotReachable("test/staticRenderableNotAnnotated/");
|
||||
assertNotReachable("test/staticRenderableNotAnnotated/valid/");
|
||||
|
||||
assertNotReachable("test/staticObjectNotAnnotated/");
|
||||
assertNotReachable("test/staticObjectNotAnnotated/valid/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStaticFieldNotAnnotated_escapeHatch() throws Exception {
|
||||
boolean currentValue = TypedFilter.PROHIBIT_STATIC_ACCESS;
|
||||
try {
|
||||
TypedFilter.PROHIBIT_STATIC_ACCESS = false;
|
||||
// to apply the new configuration
|
||||
webApp.clearMetaClassCache();
|
||||
|
||||
assertReachable("test/staticRenderableNotAnnotated/");
|
||||
assertReachable("test/staticRenderableNotAnnotated/valid/");
|
||||
|
||||
assertNotReachable("test/staticObjectNotAnnotated/");
|
||||
assertNotReachable("test/staticObjectNotAnnotated/valid/");
|
||||
} finally {
|
||||
TypedFilter.PROHIBIT_STATIC_ACCESS = currentValue;
|
||||
// to reset the configuration
|
||||
webApp.clearMetaClassCache();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStaticFieldAnnotatedOk() throws Exception {
|
||||
assertReachable("test/staticRenderableAnnotatedOk/");
|
||||
assertReachable("test/staticRenderableAnnotatedOk/valid/");
|
||||
|
||||
assertReachable("test/staticObjectAnnotatedOk/");
|
||||
assertReachable("test/staticObjectAnnotatedOk/valid/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.For;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.TestExtension;
|
||||
|
||||
@Issue("SECURITY-400")
|
||||
@For({StaplerDispatchable.class, StaplerNotDispatchable.class, TypedFilter.class})
|
||||
public class StaplerRoutableGetterTest extends StaplerAbstractTest {
|
||||
@TestExtension
|
||||
public static class TestRootAction extends AbstractUnprotectedRootAction {
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return "test";
|
||||
}
|
||||
|
||||
public Object getFalseWithoutAnnotation() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
@StaplerDispatchable
|
||||
public Object getFalseWithAnnotation() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
public Renderable getTrueWithoutAnnotation() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public Renderable getTrueWithAnnotation() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
@StaplerDispatchable
|
||||
@StaplerNotDispatchable
|
||||
public Renderable getPriorityToNegative() {
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForceGetterMethod() throws Exception {
|
||||
assertNotReachable("test/falseWithoutAnnotation/");
|
||||
assertNotReachable("test/falseWithoutAnnotation/valid/");
|
||||
|
||||
filteredGetMethodTriggered = false;
|
||||
|
||||
assertReachable("test/falseWithAnnotation/");
|
||||
assertReachable("test/falseWithAnnotation/valid/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForceNotGetterMethod() throws Exception {
|
||||
assertReachable("test/trueWithoutAnnotation/");
|
||||
assertReachable("test/trueWithoutAnnotation/valid/");
|
||||
assertNotReachable("test/trueWithAnnotation/");
|
||||
assertNotReachable("test/trueWithAnnotation/valid/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPriorityIsNegative() throws Exception {
|
||||
assertNotReachable("test/priorityToNegative/");
|
||||
}
|
||||
|
||||
public static class TestRootActionParent extends AbstractUnprotectedRootAction {
|
||||
@StaplerNotDispatchable
|
||||
public Renderable getParentKoButChildOk() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public Renderable getParentKoButChildNone() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
public Renderable getParentNoneButChildOk() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
public Renderable getParentNoneButChildKo() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
@StaplerDispatchable
|
||||
public Renderable getParentOkButChildKo() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
@StaplerDispatchable
|
||||
public Renderable getParentOkButChildNone() {
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class TestRootActionChild extends TestRootActionParent {
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return "test-child";
|
||||
}
|
||||
|
||||
@StaplerDispatchable
|
||||
public Renderable getParentKoButChildOk() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
public Renderable getParentKoButChildNone() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
@StaplerDispatchable
|
||||
public Renderable getParentNoneButChildOk() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public Renderable getParentNoneButChildKo() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public Renderable getParentOkButChildKo() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
public Renderable getParentOkButChildNone() {
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInheritanceOfAnnotation_childHasLastWord() throws Exception {
|
||||
assertNotReachable("test-child/parentKoButChildOk/");
|
||||
assertNotReachable("test-child/parentKoButChildNone/");
|
||||
|
||||
filteredGetMethodTriggered = false;
|
||||
|
||||
assertReachable("test-child/parentNoneButChildOk/");
|
||||
|
||||
assertNotReachable("test-child/parentNoneButChildKo/");
|
||||
assertNotReachable("test-child/parentOkButChildKo/");
|
||||
|
||||
filteredGetMethodTriggered = false;
|
||||
|
||||
assertReachable("test-child/parentOkButChildNone/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.For;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.recipes.LocalData;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Due to the fact we are using a @ClassRule for the other tests to improve performance,
|
||||
* we cannot use @LocalData to test the loading of the whitelist as that annotation seem to not work with @ClassRule.
|
||||
*/
|
||||
@Issue("SECURITY-400")
|
||||
@For(StaticRoutingDecisionProvider.class)
|
||||
public class StaticRoutingDecisionProvider2Test {
|
||||
|
||||
@Rule
|
||||
public JenkinsRule j = new JenkinsRule();
|
||||
|
||||
@Test
|
||||
@LocalData("whitelist_empty")
|
||||
public void userControlledWhitelist_empty_Loading() throws Exception {
|
||||
StaticRoutingDecisionProvider wl = new StaticRoutingDecisionProvider();
|
||||
assertThat(
|
||||
wl.decide("public java.lang.Object jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider.getObjectCustom()"),
|
||||
is(RoutingDecisionProvider.Decision.UNKNOWN)
|
||||
);
|
||||
assertThat(
|
||||
wl.decide("blabla"),
|
||||
is(RoutingDecisionProvider.Decision.UNKNOWN)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@LocalData("whitelist_monoline")
|
||||
public void userControlledWhitelist_monoline_Loading() throws Exception {
|
||||
StaticRoutingDecisionProvider wl = new StaticRoutingDecisionProvider();
|
||||
assertThat(
|
||||
wl.decide("method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom"),
|
||||
is(RoutingDecisionProvider.Decision.ACCEPTED)
|
||||
);
|
||||
assertThat(
|
||||
wl.decide("blabla"),
|
||||
is(RoutingDecisionProvider.Decision.UNKNOWN)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@LocalData("whitelist_multiline")
|
||||
public void userControlledWhitelist_multiline_Loading() throws Exception {
|
||||
StaticRoutingDecisionProvider wl = new StaticRoutingDecisionProvider();
|
||||
assertThat(
|
||||
wl.decide("method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom"),
|
||||
is(RoutingDecisionProvider.Decision.ACCEPTED)
|
||||
);
|
||||
assertThat(
|
||||
wl.decide("method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom2"),
|
||||
is(RoutingDecisionProvider.Decision.ACCEPTED)
|
||||
);
|
||||
assertThat(
|
||||
wl.decide("blabla"),
|
||||
is(RoutingDecisionProvider.Decision.UNKNOWN)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@LocalData("comment_ignored")
|
||||
public void userControlledWhitelist_commentsAreIgnored() throws Exception {
|
||||
StaticRoutingDecisionProvider wl = new StaticRoutingDecisionProvider();
|
||||
assertThat(wl.decide("this line is not read"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
assertThat(wl.decide("not-this-one"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
assertThat(wl.decide("neither"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
assertThat(wl.decide("finally-not"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
|
||||
assertThat(wl.decide("this-one-is"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
assertThat(wl.decide("this-one-also"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
}
|
||||
|
||||
@Test
|
||||
@LocalData("whitelist_emptyline")
|
||||
public void userControlledWhitelist_emptyLinesAreIgnored() throws Exception {
|
||||
StaticRoutingDecisionProvider wl = new StaticRoutingDecisionProvider();
|
||||
assertThat(wl.decide("signature-1"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
assertThat(wl.decide("signature-2"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
assertThat(wl.decide("signature-3"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
// neither the empty line or an exclamation mark followed by nothing or spaces are not considered
|
||||
assertThat(wl.decide(""), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
}
|
||||
|
||||
@Test
|
||||
@LocalData("greylist_multiline")
|
||||
public void userControlledWhitelist_whiteAndBlack() throws Exception {
|
||||
StaticRoutingDecisionProvider wl = new StaticRoutingDecisionProvider();
|
||||
assertThat(wl.decide("signature-1-ok"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
assertThat(wl.decide("signature-3-ok"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
|
||||
assertThat(wl.decide("signature-2-not-ok"), is(RoutingDecisionProvider.Decision.REJECTED));
|
||||
assertThat(wl.decide("signature-4-not-ok"), is(RoutingDecisionProvider.Decision.REJECTED));
|
||||
|
||||
// the exclamation mark is not used
|
||||
assertThat(wl.decide("!signature-2-not-ok"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultList() throws Exception {
|
||||
StaticRoutingDecisionProvider wl = new StaticRoutingDecisionProvider();
|
||||
|
||||
assertThat(
|
||||
wl.decide("method io.jenkins.blueocean.service.embedded.rest.AbstractRunImpl getLog"),
|
||||
is(RoutingDecisionProvider.Decision.ACCEPTED)
|
||||
);
|
||||
assertThat(
|
||||
wl.decide("method io.jenkins.blueocean.rest.impl.pipeline.PipelineNodeImpl getLog"),
|
||||
is(RoutingDecisionProvider.Decision.ACCEPTED)
|
||||
);
|
||||
assertThat(
|
||||
wl.decide("method io.jenkins.blueocean.rest.impl.pipeline.PipelineStepImpl getLog"),
|
||||
is(RoutingDecisionProvider.Decision.ACCEPTED)
|
||||
);
|
||||
|
||||
assertThat(wl.decide("method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom"),
|
||||
is(RoutingDecisionProvider.Decision.UNKNOWN)
|
||||
);
|
||||
assertThat(wl.decide("blabla"),
|
||||
is(RoutingDecisionProvider.Decision.UNKNOWN)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userControlledWhitelist_savedCorrectly() throws Exception {
|
||||
File whitelistUserControlledList = new File(j.jenkins.getRootDir(), "stapler-whitelist.txt");
|
||||
|
||||
assertFalse(whitelistUserControlledList.exists());
|
||||
|
||||
StaticRoutingDecisionProvider wl = new StaticRoutingDecisionProvider();
|
||||
|
||||
assertFalse(whitelistUserControlledList.exists());
|
||||
|
||||
assertThat(wl.decide("nothing"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
|
||||
wl.save();
|
||||
assertTrue(whitelistUserControlledList.exists());
|
||||
assertThat(FileUtils.readFileToString(whitelistUserControlledList), is(""));
|
||||
|
||||
wl.add("white-1");
|
||||
|
||||
assertThat(wl.decide("white-1"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
|
||||
assertTrue(whitelistUserControlledList.exists());
|
||||
assertThat(FileUtils.readFileToString(whitelistUserControlledList), containsString("white-1"));
|
||||
{
|
||||
StaticRoutingDecisionProvider temp = new StaticRoutingDecisionProvider();
|
||||
assertThat(temp.decide("white-1"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
}
|
||||
|
||||
wl.addBlacklistSignature("black-2");
|
||||
|
||||
assertThat(wl.decide("white-1"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
assertThat(wl.decide("black-2"), is(RoutingDecisionProvider.Decision.REJECTED));
|
||||
assertThat(FileUtils.readFileToString(whitelistUserControlledList), allOf(
|
||||
containsString("white-1"),
|
||||
containsString("!black-2")
|
||||
));
|
||||
|
||||
{
|
||||
StaticRoutingDecisionProvider temp = new StaticRoutingDecisionProvider();
|
||||
assertThat(temp.decide("white-1"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
assertThat(temp.decide("black-2"), is(RoutingDecisionProvider.Decision.REJECTED));
|
||||
}
|
||||
|
||||
wl.removeBlacklistSignature("black-2");
|
||||
|
||||
assertThat(wl.decide("white-1"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
assertThat(wl.decide("black-2"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
assertThat(FileUtils.readFileToString(whitelistUserControlledList), allOf(
|
||||
containsString("white-1"),
|
||||
not(containsString("black-2"))
|
||||
));
|
||||
|
||||
{
|
||||
StaticRoutingDecisionProvider temp = new StaticRoutingDecisionProvider();
|
||||
assertThat(temp.decide("white-1"), is(RoutingDecisionProvider.Decision.ACCEPTED));
|
||||
assertThat(temp.decide("black-2"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
}
|
||||
|
||||
wl.remove("white-1");
|
||||
|
||||
assertThat(wl.decide("white-1"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
assertThat(wl.decide("black-2"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
assertThat(FileUtils.readFileToString(whitelistUserControlledList), allOf(
|
||||
not(containsString("white-1")),
|
||||
not(containsString("black-2"))
|
||||
));
|
||||
|
||||
{
|
||||
StaticRoutingDecisionProvider temp = new StaticRoutingDecisionProvider();
|
||||
assertThat(temp.decide("white-1"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
assertThat(temp.decide("black-2"), is(RoutingDecisionProvider.Decision.UNKNOWN));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import hudson.ExtensionList;
|
||||
import hudson.model.FreeStyleProject;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.TestExtension;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.kohsuke.stapler.WebMethod;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@Issue("SECURITY-400")
|
||||
public class StaticRoutingDecisionProviderTest extends StaplerAbstractTest {
|
||||
@TestExtension
|
||||
public static class ContentProvider extends AbstractUnprotectedRootAction {
|
||||
// simulate side effect
|
||||
public static boolean called = false;
|
||||
public static boolean called2 = false;
|
||||
|
||||
public FreeStyleProject getJob() {
|
||||
called = true;
|
||||
return (FreeStyleProject) Jenkins.get().getItem("testProject");
|
||||
}
|
||||
|
||||
public String getString() {
|
||||
called = true;
|
||||
return "a";
|
||||
}
|
||||
|
||||
// cannot provide side-effect since the String has no side-effect methods
|
||||
public Object getObjectString() {
|
||||
called = true;
|
||||
return "a";
|
||||
}
|
||||
|
||||
public static String OBJECT_CUSTOM_SIGNATURE = "method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom";
|
||||
|
||||
// but it opens wide range of potentially dangerous classes
|
||||
public Object getObjectCustom() {
|
||||
called = true;
|
||||
return new Object() {
|
||||
// in order to provide a web entry-point
|
||||
public void doIndex() {
|
||||
called2 = true;
|
||||
replyOk();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
public void preparation() throws Exception {
|
||||
ContentProvider.called = false;
|
||||
ContentProvider.called2 = false;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void resetWhitelist() throws Exception {
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).resetAndSave();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_job_index() throws Exception {
|
||||
j.createFreeStyleProject("testProject");
|
||||
assertReachableWithoutOk("contentProvider/job/");
|
||||
assertTrue(ContentProvider.called);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_string() throws Exception {
|
||||
assertNotReachable("contentProvider/string/");
|
||||
assertFalse(ContentProvider.called);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_objectString() throws Exception {
|
||||
assertNotReachable("contentProvider/objectString/");
|
||||
assertFalse(ContentProvider.called);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_objectCustom() throws Exception {
|
||||
assertNotReachable("contentProvider/objectCustom/");
|
||||
assertFalse(ContentProvider.called);
|
||||
}
|
||||
|
||||
//for more test about the whitelist initial loading, please refer to StaticRoutingDecisionProvider2Test
|
||||
@Test
|
||||
public void test_objectCustom_withUserControlledSavedWhitelist() throws Throwable {
|
||||
String whitelist = ContentProvider.OBJECT_CUSTOM_SIGNATURE + "\n";
|
||||
File whitelistFile = new File(j.jenkins.getRootDir(), "stapler-whitelist.txt");
|
||||
FileUtils.write(whitelistFile, whitelist);
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).reload();
|
||||
try {
|
||||
assertNotReachable("contentProvider/objectString/");
|
||||
assertFalse(ContentProvider.called);
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
assertReachable("contentProvider/objectCustom/");
|
||||
assertTrue(ContentProvider.called);
|
||||
} finally {
|
||||
whitelistFile.delete();
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).reload();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_objectCustom_withUserControlledEditedWhitelist() throws Exception {
|
||||
try {
|
||||
assertNotReachable("contentProvider/objectString/");
|
||||
assertFalse(ContentProvider.called);
|
||||
assertNotReachable("contentProvider/objectCustom/");
|
||||
assertFalse(ContentProvider.called);
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).add(ContentProvider.OBJECT_CUSTOM_SIGNATURE);
|
||||
|
||||
assertNotReachable("contentProvider/objectString/");
|
||||
assertFalse(ContentProvider.called);
|
||||
assertFalse(ContentProvider.called2);
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
|
||||
assertReachable("contentProvider/objectCustom/");
|
||||
assertTrue(ContentProvider.called);
|
||||
assertTrue(ContentProvider.called2);
|
||||
|
||||
ContentProvider.called = false;
|
||||
ContentProvider.called2 = false;
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).remove(ContentProvider.OBJECT_CUSTOM_SIGNATURE);
|
||||
|
||||
assertNotReachable("contentProvider/objectString/");
|
||||
assertFalse(ContentProvider.called);
|
||||
assertNotReachable("contentProvider/objectCustom/");
|
||||
assertFalse(ContentProvider.called);
|
||||
} finally {
|
||||
//TODO check if the file is created per test or in general
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).reload();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_objectCustom_withStandardWhitelist() throws Exception {
|
||||
assertNotReachable("contentProvider/objectString/");
|
||||
assertFalse(ContentProvider.called);
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("contentProvider/objectCustom/");
|
||||
assertFalse(ContentProvider.called);
|
||||
|
||||
StaticRoutingDecisionProvider whitelist = ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class);
|
||||
|
||||
{// add entry in the set loaded from the standard whitelist file and reload
|
||||
Method resetMetaClassCache = StaticRoutingDecisionProvider.class.getDeclaredMethod("resetMetaClassCache");
|
||||
resetMetaClassCache.setAccessible(true);
|
||||
|
||||
Field field = StaticRoutingDecisionProvider.class.getDeclaredField("whitelistSignaturesFromFixedList");
|
||||
field.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<String> standardWhitelist = (Set<String>) field.get(whitelist);
|
||||
|
||||
standardWhitelist.add(ContentProvider.OBJECT_CUSTOM_SIGNATURE);
|
||||
// just call this method to avoid to reload the file and so override our new signature
|
||||
resetMetaClassCache.invoke(whitelist);
|
||||
}
|
||||
|
||||
assertNotReachable("contentProvider/objectString/");
|
||||
assertFalse(ContentProvider.called);
|
||||
assertFalse(ContentProvider.called2);
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
assertReachable("contentProvider/objectCustom/");
|
||||
assertTrue(ContentProvider.called);
|
||||
assertTrue(ContentProvider.called2);
|
||||
|
||||
{// reset to previous state
|
||||
ContentProvider.called = false;
|
||||
ContentProvider.called2 = false;
|
||||
|
||||
whitelist.reload();
|
||||
}
|
||||
|
||||
assertNotReachable("contentProvider/objectString/");
|
||||
assertFalse(ContentProvider.called);
|
||||
assertNotReachable("contentProvider/objectCustom/");
|
||||
assertFalse(ContentProvider.called);
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class ActionWithWhitelist extends AbstractUnprotectedRootAction {
|
||||
@Override
|
||||
public @CheckForNull String getUrlName() {
|
||||
return "do-action";
|
||||
}
|
||||
|
||||
public static String DO_ACTION_SIGNATURE = "method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ActionWithWhitelist doAction org.kohsuke.stapler.StaplerRequest";
|
||||
|
||||
public void doAction(StaplerRequest request) {
|
||||
replyOk();
|
||||
}
|
||||
|
||||
public static String DO_ACTION_STAPLER_ROUTABLE_SIGNATURE = "method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ActionWithWhitelist doActionWithStaplerDispatchable org.kohsuke.stapler.StaplerRequest";
|
||||
|
||||
@StaplerDispatchable
|
||||
public void doActionWithStaplerDispatchable(StaplerRequest request) {
|
||||
replyOk();
|
||||
}
|
||||
|
||||
public static String DO_ACTION_STAPLER_NONROUTABLE_SIGNATURE = "method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ActionWithWhitelist doActionWithStaplerNotDispatchable org.kohsuke.stapler.StaplerRequest";
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public void doActionWithStaplerNotDispatchable(StaplerRequest request) {
|
||||
replyOk();
|
||||
}
|
||||
|
||||
public static String DO_ACTION_STAPLER_WEBMETHOD_SIGNATURE = "method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ActionWithWhitelist doActionWithWebMethod org.kohsuke.stapler.StaplerRequest";
|
||||
|
||||
@WebMethod(name = "actionWithWebMethod")
|
||||
public void doActionWithWebMethod(StaplerRequest request) {
|
||||
replyOk();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doAction_regular() throws Exception {
|
||||
assertReachable("do-action/action/");
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).add(ActionWithWhitelist.DO_ACTION_SIGNATURE);
|
||||
|
||||
assertReachable("do-action/action/");
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).remove(ActionWithWhitelist.DO_ACTION_SIGNATURE);
|
||||
|
||||
assertReachable("do-action/action/");
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).addBlacklistSignature(ActionWithWhitelist.DO_ACTION_SIGNATURE);
|
||||
|
||||
assertNotReachable("do-action/action/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).removeBlacklistSignature(ActionWithWhitelist.DO_ACTION_SIGNATURE);
|
||||
|
||||
assertReachable("do-action/action/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doAction_actionWithStaplerDispatchable() throws Exception {
|
||||
assertReachable("do-action/actionWithStaplerDispatchable/");
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).addBlacklistSignature(ActionWithWhitelist.DO_ACTION_STAPLER_ROUTABLE_SIGNATURE);
|
||||
|
||||
assertReachable("do-action/actionWithStaplerDispatchable/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doAction_actionWithWebMethod() throws Exception {
|
||||
assertReachable("do-action/actionWithWebMethod/");
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).addBlacklistSignature(ActionWithWhitelist.DO_ACTION_STAPLER_WEBMETHOD_SIGNATURE);
|
||||
|
||||
assertNotReachable("do-action/actionWithWebMethod/");
|
||||
assertDoActionRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetterWithWhitelist extends AbstractUnprotectedRootAction {
|
||||
@Override
|
||||
public @CheckForNull String getUrlName() {
|
||||
return "getter";
|
||||
}
|
||||
|
||||
public static String GET_ITEM_SIGNATURE = "method jenkins.security.stapler.StaticRoutingDecisionProviderTest$GetterWithWhitelist getItem";
|
||||
|
||||
public Renderable getItem() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
public static String GET_ITEM_STAPLER_ROUTABLE_SIGNATURE = "method jenkins.security.stapler.StaticRoutingDecisionProviderTest$GetterWithWhitelist getItemWithStaplerDispatchable";
|
||||
|
||||
@StaplerDispatchable
|
||||
public Renderable getItemWithStaplerDispatchable() {
|
||||
return new Renderable();
|
||||
}
|
||||
|
||||
public static String GET_ITEM_STAPLER_NONROUTABLE_SIGNATURE = "method jenkins.security.stapler.StaticRoutingDecisionProviderTest$GetterWithWhitelist getItemWithStaplerNotDispatchable";
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public Renderable getItemWithStaplerNotDispatchable() {
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getItem_regular() throws Exception {
|
||||
assertReachable("getter/item/");
|
||||
assertReachable("getter/item/valid");
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).addBlacklistSignature(GetterWithWhitelist.GET_ITEM_SIGNATURE);
|
||||
|
||||
assertNotReachable("getter/item/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("getter/item/valid");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getItem_getterWithStaplerDispatchable() throws Exception {
|
||||
assertReachable("getter/itemWithStaplerDispatchable/");
|
||||
assertReachable("getter/itemWithStaplerDispatchable/valid");
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).addBlacklistSignature(GetterWithWhitelist.GET_ITEM_STAPLER_ROUTABLE_SIGNATURE);
|
||||
|
||||
// Annotation overrides whitelist/blacklist
|
||||
assertReachable("getter/itemWithStaplerDispatchable/");
|
||||
assertReachable("getter/itemWithStaplerDispatchable/valid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getItem_getterWithStaplerNotDispatchable() throws Exception {
|
||||
assertNotReachable("getter/itemWithStaplerNotDispatchable/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("getter/itemWithStaplerNotDispatchable/valid");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).add(GetterWithWhitelist.GET_ITEM_STAPLER_NONROUTABLE_SIGNATURE);
|
||||
|
||||
// Annotation overrides whitelist/blacklist
|
||||
assertNotReachable("getter/itemWithStaplerNotDispatchable/");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("getter/itemWithStaplerNotDispatchable/valid");
|
||||
assertGetMethodRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class FieldWithWhitelist extends AbstractUnprotectedRootAction {
|
||||
@Override
|
||||
public @CheckForNull String getUrlName() {
|
||||
return "field";
|
||||
}
|
||||
|
||||
public static String FIELD_SIGNATURE = "field jenkins.security.stapler.StaticRoutingDecisionProviderTest$FieldWithWhitelist renderable";
|
||||
|
||||
public Renderable renderable = new Renderable();
|
||||
|
||||
public static String FIELD_STAPLER_ROUTABLE_SIGNATURE = "field jenkins.security.stapler.StaticRoutingDecisionProviderTest$FieldWithWhitelist renderableWithStaplerDispatchable";
|
||||
|
||||
@StaplerDispatchable
|
||||
public Renderable renderableWithStaplerDispatchable = new Renderable();
|
||||
|
||||
public static String FIELD_STAPLER_NONROUTABLE_SIGNATURE = "field jenkins.security.stapler.StaticRoutingDecisionProviderTest$FieldWithWhitelist renderableWithStaplerNotDispatchable";
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public Renderable renderableWithStaplerNotDispatchable = new Renderable();
|
||||
|
||||
public static String FIELD_STATIC_SIGNATURE = "staticField jenkins.security.stapler.StaticRoutingDecisionProviderTest$FieldWithWhitelist staticRenderable";
|
||||
|
||||
public static Renderable staticRenderable = new Renderable();
|
||||
|
||||
public static String FIELD_STATIC_STAPLER_ROUTABLE_SIGNATURE = "staticField jenkins.security.stapler.StaticRoutingDecisionProviderTest$FieldWithWhitelist staticRenderableWithStaplerDispatchable";
|
||||
|
||||
@StaplerDispatchable
|
||||
public static Renderable staticRenderableWithStaplerDispatchable = new Renderable();
|
||||
|
||||
public static String FIELD_STATIC_STAPLER_NONROUTABLE_SIGNATURE = "staticField jenkins.security.stapler.StaticRoutingDecisionProviderTest$FieldWithWhitelist staticRenderableWithStaplerNotDispatchable";
|
||||
|
||||
@StaplerNotDispatchable
|
||||
public static Renderable staticRenderableWithStaplerNotDispatchable = new Renderable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_regular() throws Exception {
|
||||
assertReachable("field/renderable/");
|
||||
assertReachable("field/renderable/valid");
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).addBlacklistSignature(FieldWithWhitelist.FIELD_SIGNATURE);
|
||||
|
||||
assertNotReachable("field/renderable/");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("field/renderable/valid");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_regular_returnType() throws Exception {
|
||||
assertReachable("field/renderable/");
|
||||
assertReachable("field/renderable/valid");
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).addBlacklistSignature(RENDERABLE_CLASS_SIGNATURE);
|
||||
|
||||
assertNotReachable("field/renderable/");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("field/renderable/valid");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).removeBlacklistSignature(RENDERABLE_CLASS_SIGNATURE);
|
||||
|
||||
assertReachable("field/renderable/");
|
||||
assertReachable("field/renderable/valid");
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).add(RENDERABLE_CLASS_SIGNATURE);
|
||||
// method is checked first as it's more specific
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).addBlacklistSignature(FieldWithWhitelist.FIELD_SIGNATURE);
|
||||
|
||||
assertNotReachable("field/renderable/");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("field/renderable/valid");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
|
||||
// reverse, now we blacklist the type but whitelist the method => it's ok
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).remove(RENDERABLE_CLASS_SIGNATURE);
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).removeBlacklistSignature(FieldWithWhitelist.FIELD_SIGNATURE);
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).addBlacklistSignature(RENDERABLE_CLASS_SIGNATURE);
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).add(FieldWithWhitelist.FIELD_SIGNATURE);
|
||||
|
||||
assertReachable("field/renderable/");
|
||||
assertReachable("field/renderable/valid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_withStaplerDispatchable() throws Exception {
|
||||
assertReachable("field/renderableWithStaplerDispatchable/");
|
||||
assertReachable("field/renderableWithStaplerDispatchable/valid");
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).addBlacklistSignature(FieldWithWhitelist.FIELD_STAPLER_ROUTABLE_SIGNATURE);
|
||||
|
||||
assertReachable("field/renderableWithStaplerDispatchable/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_withStaplerNotDispatchable() throws Exception {
|
||||
assertNotReachable("field/renderableWithStaplerNotDispatchable/");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("field/renderableWithStaplerNotDispatchable/valid");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).add(FieldWithWhitelist.FIELD_STAPLER_NONROUTABLE_SIGNATURE);
|
||||
|
||||
assertNotReachable("field/renderableWithStaplerNotDispatchable/");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("field/renderableWithStaplerNotDispatchable/valid");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fieldStatic_regular() throws Exception {
|
||||
assertNotReachable("field/staticRenderable/");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("field/staticRenderable/valid");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).add(FieldWithWhitelist.FIELD_STATIC_SIGNATURE);
|
||||
|
||||
assertReachable("field/staticRenderable/");
|
||||
assertReachable("field/staticRenderable/valid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fieldStatic_withStaplerDispatchable() throws Exception {
|
||||
assertReachable("field/staticRenderableWithStaplerDispatchable/");
|
||||
assertReachable("field/staticRenderableWithStaplerDispatchable/valid");
|
||||
|
||||
// doesn't do anything
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).addBlacklistSignature(FieldWithWhitelist.FIELD_STATIC_STAPLER_ROUTABLE_SIGNATURE);
|
||||
|
||||
assertReachable("field/staticRenderableWithStaplerDispatchable/");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fieldStatic_withStaplerNotDispatchable() throws Exception {
|
||||
assertNotReachable("field/staticRenderableWithStaplerNotDispatchable/");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("field/staticRenderableWithStaplerNotDispatchable/valid");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
|
||||
ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class).add(FieldWithWhitelist.FIELD_STATIC_STAPLER_NONROUTABLE_SIGNATURE);
|
||||
|
||||
assertNotReachable("field/staticRenderableWithStaplerNotDispatchable/");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
assertNotReachable("field/staticRenderableWithStaplerNotDispatchable/valid");
|
||||
assertFieldRequestWasBlockedAndResetFlag();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package jenkins.security.stapler;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.TestExtension;
|
||||
import org.kohsuke.stapler.StaplerProxy;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
|
||||
@Issue("SECURITY-400")
|
||||
public class TypedFilterTest extends StaplerAbstractTest {
|
||||
@TestExtension
|
||||
public static class GetTarget1 extends AbstractUnprotectedRootAction {
|
||||
public Renderable getTarget(){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getTarget_withoutArg_isNotRoutableDirectly() throws Exception {
|
||||
assertNotReachable("getTarget1/target/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetTarget2 extends AbstractUnprotectedRootAction {
|
||||
@StaplerDispatchable
|
||||
public Renderable getTarget(){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getTarget_withoutArg_isRoutableWithAnnotation() throws Exception {
|
||||
assertReachable("getTarget2/target/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetTarget3 extends AbstractUnprotectedRootAction {
|
||||
@StaplerNotDispatchable
|
||||
public Renderable getTarget(){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getTarget_withArg_isNotRoutableWithStaplerNotDispatchable() throws Exception {
|
||||
assertNotReachable("getTarget3/target/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetTarget4 extends AbstractUnprotectedRootAction {
|
||||
public Renderable getTarget(StaplerRequest req){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getTarget_withArg_isRoutable() throws Exception {
|
||||
assertReachable("getTarget4/target/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetStaplerFallback1 extends AbstractUnprotectedRootAction {
|
||||
public Renderable getStaplerFallback(){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getStaplerFallback_withoutArg_isNotRoutableDirectly() throws Exception {
|
||||
assertNotReachable("getStaplerFallback1/staplerFallback/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetStaplerFallback2 extends AbstractUnprotectedRootAction {
|
||||
@StaplerDispatchable
|
||||
public Renderable getStaplerFallback(){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getStaplerFallback_withoutArg_isRoutableWithAnnotation() throws Exception {
|
||||
assertReachable("getStaplerFallback2/staplerFallback/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetStaplerFallback3 extends AbstractUnprotectedRootAction {
|
||||
@StaplerNotDispatchable
|
||||
public Renderable getStaplerFallback(){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getStaplerFallback_withArg_isNotRoutableWithStaplerNotDispatchable() throws Exception {
|
||||
assertNotReachable("getStaplerFallback3/staplerFallback/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetStaplerFallback4 extends AbstractUnprotectedRootAction {
|
||||
public Renderable getStaplerFallback(StaplerRequest req){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getStaplerFallback_withArg_isRoutable() throws Exception {
|
||||
assertReachable("getStaplerFallback4/staplerFallback/");
|
||||
}
|
||||
|
||||
public static class TypeImplementingStaplerProxy implements StaplerProxy {
|
||||
@Override
|
||||
public Object getTarget() {
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
public static class TypeExtendingTypeImplementingStaplerProxy extends TypeImplementingStaplerProxy {
|
||||
}
|
||||
// FIXME @StaplerNotDispatchable
|
||||
public static class TypeImplementingStaplerProxy2 implements StaplerProxy {
|
||||
@Override
|
||||
public Object getTarget() {
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
public static class TypeExtendingTypeImplementingStaplerProxy2 extends TypeImplementingStaplerProxy2 {
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetTypeImplementingStaplerProxy extends AbstractUnprotectedRootAction {
|
||||
public TypeImplementingStaplerProxy getTypeImplementingStaplerProxy(){
|
||||
return new TypeImplementingStaplerProxy();
|
||||
}
|
||||
public TypeExtendingTypeImplementingStaplerProxy getTypeExtendingTypeImplementingStaplerProxy(){
|
||||
return new TypeExtendingTypeImplementingStaplerProxy();
|
||||
}
|
||||
public TypeImplementingStaplerProxy2 getTypeImplementingStaplerProxy2(){
|
||||
return new TypeImplementingStaplerProxy2();
|
||||
}
|
||||
public TypeExtendingTypeImplementingStaplerProxy2 getTypeExtendingTypeImplementingStaplerProxy2(){
|
||||
return new TypeExtendingTypeImplementingStaplerProxy2();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeImplementingStaplerProxy_isRoutableByDefault() throws Exception {
|
||||
assertReachable("getTypeImplementingStaplerProxy/typeImplementingStaplerProxy/");
|
||||
assertReachable("getTypeImplementingStaplerProxy/typeImplementingStaplerProxy/valid");
|
||||
}
|
||||
@Test
|
||||
public void typeExtendingParentImplementingStaplerProxy_isRoutableByDefault() throws Exception {
|
||||
assertReachable("getTypeImplementingStaplerProxy/typeExtendingTypeImplementingStaplerProxy/");
|
||||
assertReachable("getTypeImplementingStaplerProxy/typeExtendingTypeImplementingStaplerProxy/valid/");
|
||||
}
|
||||
@Test
|
||||
public void typeImplementingStaplerProxy_isNotRoutableWithNonroutable() throws Exception {
|
||||
//TODO no way to avoid routability if implementing StaplerProxy
|
||||
// assertNotReachable("getTypeImplementingStaplerProxy/typeImplementingStaplerProxy2/");
|
||||
// assertNotReachable("getTypeImplementingStaplerProxy/typeImplementingStaplerProxy2/valid/");
|
||||
}
|
||||
@Test
|
||||
public void typeExtendingParentImplementingStaplerProxy_isNotRoutableWithNonroutable() throws Exception {
|
||||
//TODO no way to avoid routability if super type implementing StaplerProxy
|
||||
// assertNotReachable("getTypeImplementingStaplerProxy/typeExtendingTypeImplementingStaplerProxy2/");
|
||||
// assertNotReachable("getTypeImplementingStaplerProxy/typeExtendingTypeImplementingStaplerProxy2/valid/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetDynamic1 extends AbstractUnprotectedRootAction {
|
||||
public Renderable getDynamic(){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getDynamic_withoutArg_isRoutable() throws Exception {
|
||||
assertReachable("getDynamic1/dynamic/");
|
||||
assertNotReachable("getDynamic1/<anyString>/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetDynamic2 extends AbstractUnprotectedRootAction {
|
||||
public Renderable getDynamic(String someArgs){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getDynamic_withArgStartingWithString_isRoutable() throws Exception {
|
||||
// dynamic is "just" a subcase of regular getDynamic usage
|
||||
assertReachable("getDynamic2/dynamic/");
|
||||
assertReachable("getDynamic2/<anyString>/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetDynamic3 extends AbstractUnprotectedRootAction {
|
||||
public Renderable getDynamic(StaplerRequest req, String someArgs){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getDynamic_withArgNotStartingWithString_isNotRoutable() throws Exception {
|
||||
assertNotReachable("getDynamic3/dynamic/");
|
||||
assertNotReachable("getDynamic3/<anyString>/");
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
public static class GetDynamic4 extends AbstractUnprotectedRootAction {
|
||||
public Renderable getDynamic(StaplerRequest req){
|
||||
return new Renderable();
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void getDynamic_withArgNotIncludingString_isRoutable() throws Exception {
|
||||
assertReachable("getDynamic4/dynamic/");
|
||||
// there is no magic here, as the string argument is missing, just a regular getter
|
||||
assertNotReachable("getDynamic4/<anyString>/");
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"hudson-matrix-MatrixProject":0,"hudson-maven-MavenModuleSet":0,"hudson-model-FreeStyleProject":0,"org-jvnet-hudson-test-MockFolder":0,"org-jvnet-hudson-test-SecuredMockFolder":0}
|
||||
{"com-cloudbees-hudson-plugins-folder-Folder":0,"hudson-matrix-MatrixProject":0,"hudson-maven-MavenModuleSet":0,"hudson-model-FreeStyleProject":0,"org-jvnet-hudson-test-MockFolder":0,"org-jvnet-hudson-test-SecuredMockFolder":0}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# this line is not read
|
||||
this-one-is
|
||||
# not-this-one
|
||||
#neither
|
||||
this-one-also
|
||||
# finally-not
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
signature-1-ok
|
||||
!signature-2-not-ok
|
||||
signature-3-ok
|
||||
!signature-4-not-ok
|
||||
Whitespace-only changes.
+9
@@ -0,0 +1,9 @@
|
||||
signature-1
|
||||
# just an empty line
|
||||
|
||||
signature-2
|
||||
# space after the exclamation mark
|
||||
!
|
||||
# no space
|
||||
!
|
||||
signature-3
|
||||
+1
@@ -0,0 +1 @@
|
||||
method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom
|
||||
method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom2
|
||||
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user