VYPR
Critical severityCISA KEVNVD Advisory· Published Dec 10, 2018· Updated Oct 21, 2025

CVE-2018-1000861

CVE-2018-1000861

Description

A code execution vulnerability exists in the Stapler web framework used by Jenkins 2.153 and earlier, LTS 2.138.3 and earlier in stapler/core/src/main/java/org/kohsuke/stapler/MetaClass.java that allows attackers to invoke some methods on Java objects by accessing crafted URLs that were not intended to be invoked this way.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.jenkins-ci.main:jenkins-coreMaven
< 2.138.42.138.4
org.jenkins-ci.main:jenkins-coreMaven
>= 2.140, < 2.1542.154

Patches

1
47f38d714c99

[SECURITY-595]

https://github.com/jenkinsci/jenkinsDaniel BeckNov 21, 2018via ghsa
49 files changed · +5044 3
  • core/pom.xml+6 1 modified
    @@ -39,7 +39,7 @@ THE SOFTWARE.
     
       <properties>
         <staplerFork>true</staplerFork>
    -    <stapler.version>1.254.2</stapler.version>
    +    <stapler.version>1.254.3</stapler.version>
         <spring.version>2.5.6.SEC03</spring.version>
         <groovy.version>2.4.11</groovy.version>
         <!-- TODO: Actually many issues are being filtered by src/findbugs/findbugs-excludes.xml -->
    @@ -179,6 +179,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>
    
  • core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java+2 0 modified
    @@ -26,6 +26,7 @@
     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 HttpResponse doTest() {
             return new HttpRedirect(redirect);
         }
     
    +    @StaplerDispatchable
         public void getTestForReverseProxySetup(String rest) {
             Jenkins j = Jenkins.getInstance();
             String inferred = j.getRootUrlFromRequest() + "manage";
    
  • core/src/main/java/hudson/model/Computer.java+3 0 modified
    @@ -30,6 +30,7 @@
     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;
    @@ -958,6 +959,7 @@ public final int countExecutors() {
          * Gets the read-only snapshot view of all {@link Executor}s.
          */
         @Exported
    +    @StaplerDispatchable
         public List<Executor> getExecutors() {
             return new ArrayList<Executor>(executors);
         }
    @@ -966,6 +968,7 @@ public List<Executor> getExecutors() {
          * Gets the read-only snapshot view of all {@link OneOffExecutor}s.
          */
         @Exported
    +    @StaplerDispatchable
         public List<OneOffExecutor> getOneOffExecutors() {
             return new ArrayList<OneOffExecutor>(oneOffExecutors);
         }
    
  • core/src/main/java/hudson/model/ModelObject.java+3 0 modified
    @@ -23,6 +23,8 @@
      */
     package hudson.model;
     
    +import jenkins.security.stapler.StaplerAccessibleType;
    +
     /**
      * A model object has a human readable name.
      *
    @@ -32,6 +34,7 @@
      *
      * @author Kohsuke Kawaguchi
      */
    +@StaplerAccessibleType
     public interface ModelObject {
         String getDisplayName();
     }
    
  • core/src/main/java/hudson/model/ParameterValue.java+2 0 modified
    @@ -39,6 +39,7 @@
     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 @@
      * @see ParametersAction
      */
     @ExportedBean(defaultVisibility=3)
    +@StaplerAccessibleType
     public abstract class ParameterValue implements Serializable {
     
         private static final Logger LOGGER = Logger.getLogger(ParameterValue.class.getName());
    
  • core/src/main/java/hudson/model/Queue.java+2 0 modified
    @@ -69,6 +69,7 @@
     
     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;
    @@ -1993,6 +1994,7 @@ default Collection<? extends SubTask> getSubTasks() {
          * Implementation must have <tt>executorCell.jelly</tt>, 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.
    
  • core/src/main/java/hudson/model/UpdateCenter.java+4 0 modified
    @@ -33,6 +33,8 @@
     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;
    @@ -317,6 +319,7 @@ public void configure(UpdateCenterConfiguration config) {
          *      can be empty but never null. Oldest entries first.
          */
         @Exported
    +    @StaplerDispatchable
         public List<UpdateCenterJob> getJobs() {
             synchronized (jobs) {
                 return new ArrayList<UpdateCenterJob>(jobs);
    @@ -517,6 +520,7 @@ public HudsonUpgradeJob getHudsonJob() {
          * @return
          *      can be empty but never null.
          */
    +    @StaplerDispatchable // referenced by _api.jelly
         public PersistedList<UpdateSite> getSites() {
             return sites;
         }
    
  • core/src/main/java/hudson/model/View.java+2 0 modified
    @@ -63,6 +63,7 @@
     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;
     
    @@ -700,6 +701,7 @@ public AsynchPeople getAsynchPeople() {
         }
     
         @ExportedBean
    +    @StaplerAccessibleType
         public static final class People  {
             @Exported
             public final List<UserInfo> users;
    
  • core/src/main/java/hudson/ProxyConfiguration.java+2 0 modified
    @@ -50,6 +50,7 @@
     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 @@
      *
      * @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,
    
  • core/src/main/java/hudson/security/AuthorizationStrategy.java+2 0 modified
    @@ -36,6 +36,7 @@
     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 @@
      * @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
    
  • core/src/main/java/hudson/security/csrf/CrumbIssuer.java+2 0 modified
    @@ -9,6 +9,7 @@
     
     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 @@
      * @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";
    
  • core/src/main/java/hudson/TcpSlaveAgentListener.java+2 0 modified
    @@ -34,6 +34,7 @@
     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 @@
      * @author Kohsuke Kawaguchi
      * @see AgentProtocol
      */
    +@StaplerAccessibleType
     public final class TcpSlaveAgentListener extends Thread {
     
         private final ServerSocketChannel serverSocket;
    
  • core/src/main/java/jenkins/diagnosis/HsErrPidList.java+2 0 modified
    @@ -11,6 +11,7 @@
     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 String getDisplayName() {
         /**
          * Expose files to the URL.
          */
    +    @StaplerDispatchable
         public List<HsErrPidFile> getFiles() {
             return files;
         }
    
  • core/src/main/java/jenkins/install/InstallState.java+2 0 modified
    @@ -32,6 +32,7 @@
     import java.util.logging.Level;
     import java.util.logging.Logger;
     import jenkins.model.Jenkins;
    +import jenkins.security.stapler.StaplerAccessibleType;
     import org.apache.commons.lang.StringUtils;
     /**
      * Jenkins install state.
    @@ -44,6 +45,7 @@
      * 
      * @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
      */
    +@StaplerAccessibleType
     public class InstallState implements ExtensionPoint {
         /**
          * Need InstallState != NEW for tests by default
    
  • core/src/main/java/jenkins/model/Jenkins.java+19 0 modified
    @@ -37,7 +37,11 @@
     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;
    @@ -895,6 +899,16 @@ protected Jenkins(File root, ServletContext context, PluginManager pluginManager
                 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();
    @@ -1643,6 +1657,7 @@ public void setSystemMessage(String message) throws IOException {
             save();
         }
     
    +    @StaplerDispatchable
         public FederatedLoginService getFederatedLoginService(String name) {
             for (FederatedLoginService fls : FederatedLoginService.all()) {
                 if (fls.getUrlName().equals(name))
    @@ -2601,6 +2616,7 @@ public <T> ExtensionList<T> getExtensionList(Class<T> extensionType) {
          *
          * @since 1.349
          */
    +    @StaplerDispatchable
         public ExtensionList getExtensionList(String extensionType) throws ClassNotFoundException {
             return getExtensionList(pluginManager.uberClassLoader.loadClass(extensionType));
         }
    @@ -2970,6 +2986,7 @@ public FingerprintMap getFingerprintMap() {
         }
     
         // 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);
    @@ -4040,6 +4057,7 @@ public void doGc(StaplerResponse rsp) throws IOException {
          * End point that intentionally throws an exception to test the error behaviour.
          * @since 1.467
          */
    +    @StaplerDispatchable
         public void doException() {
             throw new RuntimeException();
         }
    @@ -4588,6 +4606,7 @@ public User getMe() {
          * 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;
         }
    
  • core/src/main/java/jenkins/security/stapler/DoActionFilter.java+133 0 added
    @@ -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;
    +    }
    +}
    
  • core/src/main/java/jenkins/security/stapler/RoutingDecisionProvider.java+38 0 added
    @@ -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);
    +}
    
  • core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java+76 0 added
    @@ -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;
    +    }
    +}
    
  • core/src/main/java/jenkins/security/stapler/StaticRoutingDecisionProvider.java+266 0 added
    @@ -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");
    +
    +}
    
  • core/src/main/java/jenkins/security/stapler/TypedFilter.java+276 0 added
    @@ -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);
    +}
    
  • core/src/main/java/jenkins/security/stapler/WebMethodConstants.java+101 0 added
    @@ -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())
    +    );
    +}
    
  • core/src/main/resources/jenkins/security/stapler/default-whitelist.txt+177 0 added
    @@ -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
    
  • core/src/test/java/jenkins/security/stapler/StaplerSignaturesTest.java+116 0 added
    @@ -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;
    +    }
    +}
    
  • test/pom.xml+6 0 modified
    @@ -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>
    
  • test/src/test/java/hudson/model/ViewTest.java+15 0 modified
    @@ -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"));
    
  • test/src/test/java/hudson/util/FormFieldValidatorTest.java+1 1 modified
    @@ -59,7 +59,7 @@ public boolean isApplicable(Class jobType) {
                     return true;
                 }
     
    -            public void doCheckXyz() {
    +            public FormValidation doCheckXyz() {
                     throw new Error("doCheckXyz is broken");
                 }
             }
    
  • test/src/test/java/jenkins/security/stapler/CustomRoutingDecisionProviderTest.java+114 0 added
    @@ -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));
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/DoActionFilterTest.java+738 0 added
    @@ -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/");
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/DynamicTest.java+73 0 added
    @@ -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";
    +        }
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/GetterMethodFilterTest.java+500 0 added
    @@ -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/");
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/JenkinsSupportAnnotationsTest.java+26 0 added
    @@ -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", "");
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/PreventRoutingTest.java+120 0 added
    @@ -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");
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/Security400Test.java+609 0 added
    @@ -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"));
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/StaplerAbstractTest.java+205 0 added
    @@ -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());
    +        }
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/StaplerRoutableActionTest.java+90 0 added
    @@ -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();
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/StaplerRoutableFieldTest.java+156 0 added
    @@ -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/");
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/StaplerRoutableGetterTest.java+172 0 added
    @@ -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/");
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/StaticRoutingDecisionProvider2Test.java+236 0 added
    @@ -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));
    +        }
    +    }
    +}
    
  • test/src/test/java/jenkins/security/stapler/StaticRoutingDecisionProviderTest.java+513 0 added
    @@ -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();
    +    }
    +}
    \ No newline at end of file
    
  • test/src/test/java/jenkins/security/stapler/TypedFilterTest.java+209 0 added
    @@ -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>/");
    +    }
    +}
    
  • test/src/test/resources/hudson/model/UsageStatisticsTest/jobs.json+1 1 modified
    @@ -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}
    \ No newline at end of file
    +{"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}
    \ No newline at end of file
    
  • test/src/test/resources/jenkins/security/stapler/StaticRoutingDecisionProvider2Test/comment_ignored/stapler-whitelist.txt+6 0 added
    @@ -0,0 +1,6 @@
    +# this line is not read
    +this-one-is
    +# not-this-one
    +#neither
    +this-one-also
    +# finally-not
    
  • test/src/test/resources/jenkins/security/stapler/StaticRoutingDecisionProvider2Test/greylist_multiline/stapler-whitelist.txt+4 0 added
    @@ -0,0 +1,4 @@
    +signature-1-ok
    +!signature-2-not-ok
    +signature-3-ok
    +!signature-4-not-ok
    
  • test/src/test/resources/jenkins/security/stapler/StaticRoutingDecisionProvider2Test/whitelist_emptyline/stapler-whitelist.txt+9 0 added
    @@ -0,0 +1,9 @@
    +signature-1
    +# just an empty line
    +
    +signature-2
    +# space after the exclamation mark
    +!      
    +# no space
    +!
    +signature-3
    
  • test/src/test/resources/jenkins/security/stapler/StaticRoutingDecisionProvider2Test/whitelist_empty/stapler-whitelist.txt+0 0 added
  • test/src/test/resources/jenkins/security/stapler/StaticRoutingDecisionProvider2Test/whitelist_monoline/stapler-whitelist.txt+1 0 added
    @@ -0,0 +1 @@
    +method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom
    
  • test/src/test/resources/jenkins/security/stapler/StaticRoutingDecisionProvider2Test/whitelist_multiline/stapler-whitelist.txt+2 0 added
    @@ -0,0 +1,2 @@
    +method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom
    +method jenkins.security.stapler.StaticRoutingDecisionProviderTest$ContentProvider getObjectCustom2
    
  • test/src/test/resources/plugins/annotations-test.hpi+0 0 added
  • test/src/test/resources/plugins/annotations-test-sources.jar+0 0 added

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.