Merge branch 'security-stable-2.150' into security-master

This commit is contained in:
Daniel Beck committed 2018-11-21 15:42:46 +01:00
commit db5defdf2f
49 files changed
+5045 -3

No files matched your search

+6 -1
View File
@@ -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;
}
}
+6
View File
@@ -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}
@@ -0,0 +1,6 @@
# this line is not read
this-one-is
# not-this-one
#neither
this-one-also
# finally-not
@@ -0,0 +1,4 @@
signature-1-ok
!signature-2-not-ok
signature-3-ok
!signature-4-not-ok
@@ -0,0 +1,9 @@
signature-1
# just an empty line
signature-2
# space after the exclamation mark
!
# no space
!
signature-3
@@ -0,0 +1 @@
method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom
@@ -0,0 +1,2 @@
method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom
method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom2
Binary file not shown.