CVE-2019-10354
Description
A vulnerability in the Stapler web framework used by Jenkins allows attackers to access view fragments directly, bypassing permission checks and potentially leaking sensitive information.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A vulnerability in the Stapler web framework used by Jenkins allows attackers to access view fragments directly, bypassing permission checks and potentially leaking sensitive information.
Vulnerability
Jenkins uses the Stapler web framework to render its UI. Views are composed of multiple fragments, which plugins can extend. However, CVE-2019-10354 arises because these view fragments were accessible via direct URL requests without proper permission checks [1][4]. The root cause was found in the Stapler dispatch logic, where view fragment dispatchers did not enforce authorization [2].
Exploitation
An attacker can directly request a view fragment URL, bypassing the access controls normally applied to the full page. No special privileges are required; the attacker only needs network access to a Jenkins instance running a vulnerable version [1]. The vulnerability exists in Jenkins 2.185 and earlier, and LTS 2.176.1 and earlier [1][3].
Impact
Successful exploitation allows an attacker to access view fragments that may contain sensitive information, such as configuration details or internal data. The impact is confidentiality breach [4]. The CVSSv3 base score is 5.3 (Medium) [1].
Mitigation
The fix was released in Jenkins 2.186 and LTS 2.176.2, which update the Stapler dependency to version 1.256.1 [3][4]. Users should upgrade immediately. No workarounds are documented; the advisory recommends upgrading to the patched versions [4].
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.main:jenkins-coreMaven | < 2.176.2 | 2.176.2 |
org.jenkins-ci.main:jenkins-coreMaven | >= 2.177, < 2.186 | 2.186 |
org.kohsuke.stapler:stapler-parentMaven | < 1.257.1 | 1.257.1 |
Affected products
3- ghsa-coords2 versions
< 2.176.2+ 1 more
- (no CPE)range: < 2.176.2
- (no CPE)range: < 1.257.1
- Range: 2.185 and earlier, LTS 2.176.1 and earlier
Patches
248 files changed · +1312 −68
core/pom.xml+2 −2 modified@@ -39,7 +39,7 @@ THE SOFTWARE. <properties> <staplerFork>true</staplerFork> - <stapler.version>1.256</stapler.version> + <stapler.version>1.256.1</stapler.version> <spring.version>2.5.6.SEC03</spring.version> <groovy.version>2.4.11</groovy.version> </properties> @@ -180,7 +180,7 @@ THE SOFTWARE. <dependency> <groupId>io.jenkins.stapler</groupId> <artifactId>jenkins-stapler-support</artifactId> - <version>1.0</version> + <version>1.1</version> </dependency> <dependency> <groupId>org.hamcrest</groupId>
core/src/main/java/hudson/Functions.java+1 −0 modified@@ -240,6 +240,7 @@ public static boolean isExtensionsAvailable() { public static void initPageVariables(JellyContext context) { StaplerRequest currentRequest = Stapler.getCurrentRequest(); + currentRequest.getWebApp().getDispatchValidator().allowDispatch(currentRequest, Stapler.getCurrentResponse()); String rootURL = currentRequest.getContextPath(); Functions h = new Functions();
core/src/main/java/hudson/widgets/RenderOnDemandClosure.java+1 −0 modified@@ -95,6 +95,7 @@ public RenderOnDemandClosure(JellyContext context, String attributesToCapture) { public HttpResponse render() { return new HttpResponse() { public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { + req.getWebApp().getDispatchValidator().allowDispatch(req, rsp); try { new DefaultScriptInvoker() { @Override
core/src/main/java/jenkins/model/Jenkins.java+6 −2 modified@@ -39,6 +39,7 @@ import jenkins.AgentProtocol; import jenkins.diagnostics.URICheckEncodingMonitor; import jenkins.security.stapler.DoActionFilter; +import jenkins.security.stapler.StaplerDispatchValidator; import jenkins.security.stapler.StaplerFilteredActionListener; import jenkins.security.stapler.StaplerDispatchable; import jenkins.security.RedactSecretJsonInErrorMessageSanitizer; @@ -913,6 +914,9 @@ protected Jenkins(File root, ServletContext context, PluginManager pluginManager webApp.setFilteredDoActionTriggerListener(actionListener); webApp.setFilteredFieldTriggerListener(actionListener); + webApp.setDispatchValidator(new StaplerDispatchValidator()); + webApp.setFilteredDispatchTriggerListener(actionListener); + adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader,"adjuncts/"+SESSION_HASH, TimeUnit.DAYS.toMillis(365)); ClassFilterImpl.register(); @@ -4416,9 +4420,9 @@ public static void _doScript(StaplerRequest req, StaplerResponse rsp, RequestDis @RequirePOST public void doEval(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { checkPermission(RUN_SCRIPTS); - + req.getWebApp().getDispatchValidator().allowDispatch(req, rsp); try { - MetaClass mc = WebApp.getCurrent().getMetaClass(getClass()); + MetaClass mc = req.getWebApp().getMetaClass(getClass()); Script script = mc.classLoader.loadTearOff(JellyClassLoaderTearOff.class).createContext().compileScript(new InputSource(req.getReader())); new JellyRequestDispatcher(this,script).forward(req,rsp); } catch (JellyException e) {
core/src/main/java/jenkins/security/stapler/StaplerDispatchValidator.java+354 −0 added@@ -0,0 +1,354 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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 jenkins.model.Jenkins; +import jenkins.util.SystemProperties; +import org.apache.commons.io.IOUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.CancelRequestHandlingException; +import org.kohsuke.stapler.DispatchValidator; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.WebApp; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.servlet.ServletContext; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Validates views dispatched by Stapler. This validation consists of two phases: + * <ul> + * <li>Before views are loaded, the model class is checked for {@link StaplerViews}/{@link StaplerFragments} along + * with whitelist entries specified by the default views whitelist and the optionally defined whitelist specified + * by the system property {@code jenkins.security.stapler.StaplerDispatchValidator.whitelist}. Then, + * the model class's superclass and interfaces are recursively inspected adding views and fragments that do not + * conflict with the views and fragments already declared. This effectively allows model classes to override + * parent classes.</li> + * <li>Before views write any response output, this validator is checked to see if the view has declared itself + * dispatchable using the {@code l:view} Jelly tag. As this validation comes later, annotations will take + * precedence over the use or lack of a layout tag.</li> + * </ul> + * <p>Validation can be disabled by setting the system property + * {@code jenkins.security.stapler.StaplerDispatchValidator.disabled=true} or setting {@link #DISABLED} to + * {@code true} in the script console.</p> + * + * @since TODO + */ +@Restricted(NoExternalUse.class) +public class StaplerDispatchValidator implements DispatchValidator { + + private static final Logger LOGGER = Logger.getLogger(StaplerDispatchValidator.class.getName()); + private static final String ATTRIBUTE_NAME = StaplerDispatchValidator.class.getName() + ".status"; + private static final String ESCAPE_HATCH = StaplerDispatchValidator.class.getName() + ".disabled"; + /** + * Escape hatch to disable dispatch validation. + */ + public static /* script-console editable */ boolean DISABLED = SystemProperties.getBoolean(ESCAPE_HATCH); + + private static @CheckForNull Boolean setStatus(@Nonnull StaplerRequest req, @CheckForNull Boolean status) { + if (status == null) { + return null; + } + LOGGER.fine(() -> "Request dispatch set status to " + status + " for URL " + req.getPathInfo()); + req.setAttribute(ATTRIBUTE_NAME, status); + return status; + } + + private static @CheckForNull Boolean computeStatusIfNull(@Nonnull StaplerRequest req, @Nonnull Supplier<Boolean> statusIfNull) { + Object requestStatus = req.getAttribute(ATTRIBUTE_NAME); + return requestStatus instanceof Boolean ? (Boolean) requestStatus : setStatus(req, statusIfNull.get()); + } + + private final ValidatorCache cache; + + public StaplerDispatchValidator() { + cache = new ValidatorCache(); + cache.load(); + } + + @Override + public @CheckForNull Boolean isDispatchAllowed(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp) { + if (DISABLED) { + return true; + } + Boolean status = computeStatusIfNull(req, () -> { + if (rsp.getContentType() != null) { + return true; + } + if (rsp.getStatus() >= 300) { + return true; + } + return null; + }); + LOGGER.finer(() -> req.getRequestURI() + " -> " + status); + return status; + } + + @Override + public @CheckForNull Boolean isDispatchAllowed(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp, @Nonnull String viewName, @CheckForNull Object node) { + if (DISABLED) { + return true; + } + Boolean status = computeStatusIfNull(req, () -> { + if (viewName.equals("index")) { + return true; + } + if (node == null) { + return null; + } + return cache.find(node.getClass()).isViewValid(viewName); + }); + LOGGER.finer(() -> "<" + req.getRequestURI() + ", " + viewName + ", " + node + "> -> " + status); + return status; + } + + @Override + public void allowDispatch(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp) { + if (DISABLED) { + return; + } + setStatus(req, true); + } + + @Override + public void requireDispatchAllowed(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp) throws CancelRequestHandlingException { + if (DISABLED) { + return; + } + Boolean status = isDispatchAllowed(req, rsp); + if (status == null || !status) { + LOGGER.fine(() -> "Cancelling dispatch for " + req.getRequestURI()); + throw new CancelRequestHandlingException(); + } + } + + @VisibleForTesting + static StaplerDispatchValidator getInstance(@Nonnull ServletContext context) { + return (StaplerDispatchValidator) WebApp.get(context).getDispatchValidator(); + } + + @VisibleForTesting + void loadWhitelist(@Nonnull InputStream in) throws IOException { + cache.loadWhitelist(IOUtils.readLines(in)); + } + + private static class ValidatorCache { + private final Map<String, Validator> validators = new HashMap<>(); + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + // provided as alternative to ConcurrentHashMap.computeIfAbsent which doesn't allow for recursion in the supplier + // see https://stackoverflow.com/q/28840047 + private @Nonnull Validator computeIfAbsent(@Nonnull String className, @Nonnull Function<String, Validator> constructor) { + lock.readLock().lock(); + try { + if (validators.containsKey(className)) { + // cached value + return validators.get(className); + } + } finally { + lock.readLock().unlock(); + } + lock.writeLock().lock(); + try { + if (validators.containsKey(className)) { + // cached between readLock.unlock and writeLock.lock + return validators.get(className); + } + Validator value = constructor.apply(className); + validators.put(className, value); + return value; + } finally { + lock.writeLock().unlock(); + } + } + + private @Nonnull Validator find(@Nonnull Class<?> clazz) { + return computeIfAbsent(clazz.getName(), name -> create(clazz)); + } + + private @Nonnull Validator find(@Nonnull String className) { + return computeIfAbsent(className, this::create); + } + + private @Nonnull Collection<Validator> findParents(@Nonnull Class<?> clazz) { + List<Validator> parents = new ArrayList<>(); + Class<?> superclass = clazz.getSuperclass(); + if (superclass != null) { + parents.add(find(superclass)); + } + for (Class<?> iface : clazz.getInterfaces()) { + parents.add(find(iface)); + } + return parents; + } + + private @Nonnull Validator create(@Nonnull Class<?> clazz) { + Set<String> allowed = new HashSet<>(); + StaplerViews views = clazz.getDeclaredAnnotation(StaplerViews.class); + if (views != null) { + allowed.addAll(Arrays.asList(views.value())); + } + Set<String> denied = new HashSet<>(); + StaplerFragments fragments = clazz.getDeclaredAnnotation(StaplerFragments.class); + if (fragments != null) { + denied.addAll(Arrays.asList(fragments.value())); + } + return new Validator(() -> findParents(clazz), allowed, denied); + } + + private @Nonnull Validator create(@Nonnull String className) { + ClassLoader loader = Jenkins.get().pluginManager.uberClassLoader; + return new Validator(() -> { + try { + return findParents(loader.loadClass(className)); + } catch (ClassNotFoundException e) { + LOGGER.log(Level.WARNING, e, () -> "Could not load class " + className + " to validate views"); + return Collections.emptySet(); + } + }); + } + + private void load() { + try { + try (InputStream is = Validator.class.getResourceAsStream("default-views-whitelist.txt")) { + loadWhitelist(IOUtils.readLines(is, StandardCharsets.UTF_8)); + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not load default views whitelist", e); + } + String whitelist = SystemProperties.getString(StaplerDispatchValidator.class.getName() + ".whitelist"); + Path configFile = whitelist != null ? Paths.get(whitelist) : Jenkins.get().getRootDir().toPath().resolve("stapler-views-whitelist.txt"); + if (Files.exists(configFile)) { + try { + loadWhitelist(Files.readAllLines(configFile)); + } catch (IOException e) { + LOGGER.log(Level.WARNING, e, () -> "Could not load user defined whitelist from " + configFile); + } + } + } + + private void loadWhitelist(@Nonnull List<String> whitelistLines) { + for (String line : whitelistLines) { + if (line.matches("#.*|\\s*")) { + // commented line + continue; + } + String[] parts = line.split("\\s+"); + if (parts.length < 2) { + // invalid input format + LOGGER.warning(() -> "Cannot update validator with malformed line: " + line); + continue; + } + Validator validator = find(parts[0]); + for (int i = 1; i < parts.length; i++) { + String view = parts[i]; + if (view.startsWith("!")) { + validator.denyView(view.substring(1)); + } else { + validator.allowView(view); + } + } + } + } + + private class Validator { + // lazy load parents to avoid trying to load potentially unavailable classes + private final Supplier<Collection<Validator>> parentsSupplier; + private volatile Collection<Validator> parents; + private final Set<String> allowed = ConcurrentHashMap.newKeySet(); + private final Set<String> denied = ConcurrentHashMap.newKeySet(); + + private Validator(@Nonnull Supplier<Collection<Validator>> parentsSupplier) { + this.parentsSupplier = parentsSupplier; + } + + private Validator(@Nonnull Supplier<Collection<Validator>> parentsSupplier, @Nonnull Collection<String> allowed, @Nonnull Collection<String> denied) { + this(parentsSupplier); + this.allowed.addAll(allowed); + this.denied.addAll(denied); + } + + private @Nonnull Collection<Validator> getParents() { + if (parents == null) { + synchronized (this) { + if (parents == null) { + parents = parentsSupplier.get(); + } + } + } + return parents; + } + + private @CheckForNull Boolean isViewValid(@Nonnull String viewName) { + if (allowed.contains(viewName)) { + return Boolean.TRUE; + } + if (denied.contains(viewName)) { + return Boolean.FALSE; + } + for (Validator parent : getParents()) { + Boolean result = parent.isViewValid(viewName); + if (result != null) { + return result; + } + } + return null; + } + + private void allowView(@Nonnull String viewName) { + allowed.add(viewName); + } + + private void denyView(@Nonnull String viewName) { + denied.add(viewName); + } + } + } +}
core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java+11 −2 modified@@ -28,6 +28,7 @@ import org.kohsuke.stapler.Function; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.event.FilteredDispatchTriggerListener; import org.kohsuke.stapler.event.FilteredDoActionTriggerListener; import org.kohsuke.stapler.event.FilteredFieldTriggerListener; import org.kohsuke.stapler.event.FilteredGetterTriggerListener; @@ -37,10 +38,10 @@ import java.util.logging.Logger; /** - * Log a warning message when a "getter" or "doAction" function that was filtered out by SECURITY-400 new rules + * Log a warning message when a "getter" or "doAction" function or fragment view that was filtered out by SECURITY-400 new rules */ @Restricted(NoExternalUse.class) -public class StaplerFilteredActionListener implements FilteredDoActionTriggerListener, FilteredGetterTriggerListener, FilteredFieldTriggerListener { +public class StaplerFilteredActionListener implements FilteredDoActionTriggerListener, FilteredGetterTriggerListener, FilteredFieldTriggerListener, FilteredDispatchTriggerListener { 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. " + @@ -73,4 +74,12 @@ public boolean onFieldTrigger(FieldRef f, StaplerRequest req, StaplerResponse st }); return false; } + + @Override + public boolean onDispatchTrigger(StaplerRequest req, StaplerResponse rsp, Object node, String viewName) { + LOGGER.warning(() -> "New Stapler dispatch rules result in the URL \"" + req.getPathInfo() + "\" no longer being allowed. " + + "If you consider it safe to use, add the following to the whitelist: \"" + node.getClass().getName() + " " + viewName + "\". "+ + "Learn more: https://jenkins.io/redirect/stapler-facet-restrictions"); + return false; + } }
core/src/main/resources/hudson/atom.jelly+1 −3 modified@@ -23,9 +23,7 @@ THE SOFTWARE. --> <?jelly escape-by-default='true'?> -<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson"><!-- No whitespace before xml header: --><?xml version="1.0" encoding="UTF-8"?> - <st:contentType value="application/atom+xml;charset=UTF-8" /> - <j:new var="h" className="hudson.Functions" /><!-- instead of JSP functions --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"><st:contentType value="application/atom+xml;charset=UTF-8" /><!-- No whitespace before xml header: --><?xml version="1.0" encoding="UTF-8"?> <!-- ATOM. See http://atompub.org/rfc4287.html for the format --> <feed xmlns="http://www.w3.org/2005/Atom">
core/src/main/resources/hudson/model/Computer/_scriptText.jelly+5 −2 modified@@ -26,5 +26,8 @@ THE SOFTWARE. Called from doScriptText() to display the execution result. --> <?jelly escape-by-default='true'?> -<st:compress xmlns:j="jelly:core" xmlns:st="jelly:stapler"> -<st:contentType value="text/plain;charset=UTF-8" /><j:out value="${output}"/></st:compress> +<st:compress xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout"> + <l:view contentType="text/plain;charset=UTF-8"> + <j:out value="${output}"/> + </l:view> +</st:compress>
core/src/main/resources/hudson/model/View/cc.xml.jelly+3 −4 modified@@ -27,10 +27,8 @@ THE SOFTWARE. See http://confluence.public.thoughtworks.org/display/CI/Multiple+Project+Summary+Reporting+Standard --> <?jelly escape-by-default='true'?> -<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt"> - <st:contentType value="text/xml;charset=UTF-8" /> - <j:new var="h" className="hudson.Functions" /> - +<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout"> + <l:view contentType="text/xml;charset=UTF-8"> <Projects> <j:forEach var="p" items="${h.getCCItems(it)}"> <j:set var="lb" value="${p.lastCompletedBuild}"/> @@ -45,4 +43,5 @@ THE SOFTWARE. </j:if> </j:forEach> </Projects> + </l:view> </j:jelly> \ No newline at end of file
core/src/main/resources/hudson/rss20.jelly+2 −4 modified@@ -23,11 +23,9 @@ THE SOFTWARE. --> <?jelly escape-by-default='true'?> -<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson"><!-- No whitespace before xml header: --><?xml version="1.0" encoding="UTF-8"?> - <st:contentType value="text/xml;charset=UTF-8" /> - <j:new var="h" className="hudson.Functions" /><!-- instead of JSP functions --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"><st:contentType value="text/xml;charset=UTF-8" /><!-- No whitespace before xml header: --><?xml version="1.0" encoding="UTF-8"?> - <!-- RSS 2.0 feed. See http://cyber.law.harvard.edu/rss/rss.html for the format --> + <!-- RSS 2.0 feed. See http://cyber.law.harvard.edu/rss/rss.html for the format --> <rss version="2.0"> <channel> <title>${title}</title>
core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly+2 −6 modified@@ -27,19 +27,14 @@ THE SOFTWARE. --> <?jelly escape-by-default='true'?> <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:x="jelly:xml" xmlns:f="/lib/form"> - + <l:view xmlns:l="/lib/layout" contentType="text/html;charset=UTF-8"> <st:setHeader name="Expires" value="0"/> <st:setHeader name="Cache-Control" value="no-cache,no-store,must-revalidate"/> - <j:new var="h" className="hudson.Functions"/><!-- instead of JSP functions --> - <!-- response contentType header --> - <st:contentType value="text/html;charset=UTF-8"/> <!-- needed to generate a session if non exists, without it we would add ";jsessionid=" to the url which will result in a 404 --> <j:set var="_" value="${request.getSession()}"/> - <!-- get default/commong page variable --> - ${h.initPageVariables(context)} <x:doctype name="html"/> <!-- in case of error we want to surround the form elements with an error hint --> <j:set var="inputClass" value="${data.errorMessage!=null ? 'danger' : 'normal'}"/> @@ -297,4 +292,5 @@ THE SOFTWARE. </div> </body> </html> + </l:view> </j:jelly>
core/src/main/resources/hudson/slaves/SlaveComputer/slave-agent.jnlp.jelly+2 −2 modified@@ -24,8 +24,7 @@ THE SOFTWARE. <?jelly escape-by-default='true'?> <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson"> - <st:contentType value="application/x-java-jnlp-file" /> - <j:new var="h" className="hudson.Functions" /> + <l:view contentType="application/x-java-jnlp-file"> <j:set var="rootURL" value="${app.rootUrl}" /> <!-- @@ -104,4 +103,5 @@ THE SOFTWARE. </j:if> </application-desc> </jnlp> + </l:view> </j:jelly>
core/src/main/resources/jenkins/model/Jenkins/login.jelly+2 −3 modified@@ -24,19 +24,17 @@ THE SOFTWARE. <?jelly escape-by-default='true'?> <j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:x="jelly:xml" xmlns:st="jelly:stapler"> + <l:view xmlns:l="/lib/layout" contentType="text/html;charset=UTF-8"> <!-- deactivate all caching --> <st:setHeader name="Expires" value="0" /> <st:setHeader name="Cache-Control" value="no-cache,no-store,must-revalidate" /> - <j:new var="h" className="hudson.Functions" /><!-- instead of JSP functions --> <!-- needed for cli --> <j:if test="${request.servletPath=='/' || request.servletPath==''}"> ${h.advertiseHeaders(response)} <j:forEach var="pd" items="${h.pageDecorators}"> <st:include it="${pd}" page="httpHeaders.jelly" optional="true"/> </j:forEach> </j:if> - <!-- response contentType header --> - <st:contentType value="text/html;charset=UTF-8" /> <!-- needed to generate a session if non exists, without it we would add ";jsessionid=" to the url which will result in a 404 @@ -155,4 +153,5 @@ THE SOFTWARE. </j:choose> </body> </html> + </l:view> </j:jelly>
core/src/main/resources/jenkins/model/Jenkins/opensearch.xml.jelly+4 −4 modified@@ -26,10 +26,9 @@ THE SOFTWARE. OpenSearch description for using search box from browsers --> <?jelly escape-by-default='true'?> -<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" -><st:contentType value="application/xml;charset=UTF-8" -/><OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> - <j:new var="h" className="hudson.Functions" /><!-- instead of JSP functions --> +<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout"> + <l:view contentType="application/xml;charset=UTF-8"> + <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> <j:set var="rootURL" value="${h.inferHudsonURL(request)}"/> <ShortName>Jenkins</ShortName> <Description>Jenkins at ${rootURL}</Description> @@ -38,4 +37,5 @@ THE SOFTWARE. <Url type="text/html" method="GET" template="${rootURL}search/?q={searchTerms}" /> <Url type="application/x-suggestions+json" template="${rootURL}search/suggestOpenSearch?q={searchTerms}"/> </OpenSearchDescription> + </l:view> </j:jelly> \ No newline at end of file
core/src/main/resources/jenkins/model/Jenkins/_scriptText.jelly+5 −2 modified@@ -26,5 +26,8 @@ THE SOFTWARE. Called from doScriptText() to display the execution result. --> <?jelly escape-by-default='true'?> -<st:compress xmlns:j="jelly:core" xmlns:st="jelly:stapler"> -<st:contentType value="text/plain;charset=UTF-8" /><j:out value="${output}"/></st:compress> +<st:compress xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout"> + <l:view contentType="text/plain;charset=UTF-8"> + <j:out value="${output}"/> + </l:view> +</st:compress>
core/src/main/resources/jenkins/security/stapler/default-views-whitelist.txt+34 −0 added@@ -0,0 +1,34 @@ +# This file contains the built-in whitelist of allows views for Stapler dispatch filtering. +# This is provided as part of SECURITY-534 for backward compatibility. + +# Format: fully qualified class name of model class, a space, and then a space-separated list of view names to allow or deny (prefix with !) +# * class names can be repeated +# * subclasses of classes listed here are also whitelisted with the same view names +# Example: +# io.jenkins.plugins.example.ModelClass viewA viewB !viewC + +# https://plugins.jenkins.io/gerrit-trigger +com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger.actions.manual.ManualTriggerAction help-Search + +# https://plugins.jenkins.io/depgraph-view +# This works experimentally even without this entry, but probably only because of buffer size, so be safe +hudson.plugins.depgraph_view.AbstractDependencyGraphAction jsplumb + +# https://plugins.jenkins.io/multi-slave-config-plugin +com.sonyericsson.hudson.plugins.multislaveconfigplugin.NodeManageLink slavefilter + +# https://plugins.jenkins.io/coverity +jenkins.plugins.coverity.CheckConfig$DescriptorImpl checkConfig +jenkins.plugins.coverity.CIMStream$DescriptorImpl defectFilters + +# https://plugins.jenkins.io/sounds +net.hurstfrost.hudson.sounds.SoundsAgentAction jsonpdemo + +# https://plugins.jenkins.io/scm2job +com.unitedinternet.jenkins.plugins.scm2job.SCM2Job showResultsPlain + +# https://plugins.jenkins.io/serenity +com.ikokoon.serenity.hudson.SerenityProjectAction nocoverage + +# https://plugins.jenkins.io/pipeline-config-history +org.jenkinsci.plugins.pipelineConfigHistory.view.PipelineConfigHistoryProjectAction configSingleFile
core/src/main/resources/lib/layout/ajax.jelly+3 −6 modified@@ -50,13 +50,10 @@ THE SOFTWARE. </j:when> <j:otherwise> <!-- called to generate partial HTML. set up HTML headers and etc --> - <!-- copied from layout.jelly --> - <st:contentType value="${attrs.contentType?:'text/html;charset=UTF-8'}" /> - <j:new var="h" className="hudson.Functions" /><!-- instead of JSP functions --> - ${h.initPageVariables(context)} <j:set var="ajax" value="true"/> - - <d:invokeBody/> + <l:view contentType="${attrs.contentType?:'text/html;charset=UTF-8'}"> + <d:invokeBody/> + </l:view> </j:otherwise> </j:choose> </j:jelly> \ No newline at end of file
core/src/main/resources/lib/layout/html.jelly+2 −3 modified@@ -56,13 +56,11 @@ THE SOFTWARE. (The permission will be checked against the "it" object.) </st:attribute> </st:documentation> +<l:view contentType="text/html;charset=UTF-8"> <st:setHeader name="Expires" value="0" /> <st:setHeader name="Cache-Control" value="no-cache,no-store,must-revalidate" /> <st:setHeader name="X-Hudson-Theme" value="default" /> -<st:contentType value="text/html;charset=UTF-8" /> - <j:new var="h" className="hudson.Functions" /><!-- instead of JSP functions --> -${h.initPageVariables(context)} <!-- depending on what tags are used, we can later end up discovering that we needed a session, but it's too late because the headers are already committed. so ensure we always have a session. @@ -171,4 +169,5 @@ ${h.initPageVariables(context)} </div> </body> </html> +</l:view> </j:jelly>
core/src/main/resources/lib/layout/layout.jelly+2 −3 modified@@ -56,14 +56,12 @@ THE SOFTWARE. Available values: two-column (by default), one-column (full-width size) or full-screen (since 2.53). </st:attribute> </st:documentation> +<l:view contentType="text/html;charset=UTF-8"> <st:setHeader name="Expires" value="0" /> <st:setHeader name="Cache-Control" value="no-cache,no-store,must-revalidate" /> <st:setHeader name="X-Hudson-Theme" value="default" /> <st:setHeader name="Referrer-Policy" value="same-origin" /> -<st:contentType value="text/html;charset=UTF-8" /> - <j:new var="h" className="hudson.Functions" /><!-- instead of JSP functions --> -${h.initPageVariables(context)} <!-- depending on what tags are used, we can later end up discovering that we needed a session, but it's too late because the headers are already committed. so ensure we always have a session. @@ -293,4 +291,5 @@ ${h.initPageVariables(context)} </body> </html> +</l:view> </j:jelly>
core/src/main/resources/lib/layout/view.jelly+40 −0 added@@ -0,0 +1,40 @@ +<?jelly escape-by-default='true'?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> + +<j:jelly xmlns:j="jelly:core" xmlns:d="jelly:define" xmlns:st="jelly:stapler"> + <st:documentation> + Declares a view as dispatchable. Views that do not use this tag or another tag that invokes this are + considered fragment views and are not dispatchable. + <st:attribute name="contentType"> + HTTP Content-Type header value to use for this view. + </st:attribute> + </st:documentation> + <j:if test="${attrs.contentType != null}"> + <st:contentType value="${attrs.contentType}"/> + </j:if> + <j:new var="h" className="hudson.Functions"/> + ${h.initPageVariables(context)} + <d:invokeBody/> +</j:jelly>
test/src/test/java/jenkins/security/stapler/Security867Test.java+2 −0 modified@@ -120,6 +120,7 @@ public void folderTraversalPrevented_avoidStealingSecretFromDifferentObject() th } @TestExtension + @StaplerViews("public") public static class RootAction1 implements RootAction { // not displayed in its own public.jelly public String getMyConfig() { @@ -143,6 +144,7 @@ public String getMyConfig() { } @TestExtension + @StaplerViews("showConfig") public static class RootAction3 implements RootAction { // displayed in its showConfig.jelly public String getMyConfig() {
test/src/test/java/jenkins/security/stapler/StaplerDispatchValidatorTest.java+179 −0 added@@ -0,0 +1,179 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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.html.HtmlPage; +import hudson.model.UnprotectedRootAction; +import org.junit.Before; +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 javax.annotation.CheckForNull; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; + +@Issue("SECURITY-534") +public class StaplerDispatchValidatorTest { + + @Rule public JenkinsRule j = new JenkinsRule(); + + @Before + public void setUp() throws Exception { + StaplerDispatchValidator validator = StaplerDispatchValidator.getInstance(j.jenkins.servletContext); + try (InputStream whitelist = getClass().getResourceAsStream("StaplerDispatchValidatorTest/whitelist.txt")) { + validator.loadWhitelist(whitelist); + } + } + + @Test + @For(StaplerViews.class) + public void canViewStaplerViews() throws Exception { + String[] urls = {"annotated/explicitRoot", "extended/explicitRoot", "extended/whitelistedRoot"}; + for (String url : urls) { + HtmlPage root = j.createWebClient().goTo(url); + assertEquals("Fragment", root.getElementById("frag").asText()); + assertEquals("Explicit Fragment", root.getElementById("explicit-frag").asText()); + } + } + + @Test + @For(StaplerFragments.class) + public void cannotViewStaplerFragments() throws Exception { + String[] urls = {"annotated/explicitFrag", "extended/explicitFrag"}; + for (String url : urls) { + j.createWebClient().assertFails(url, HttpURLConnection.HTTP_NOT_FOUND); + } + } + + @Test + public void canViewRoot() throws Exception { + String[] urls = {"annotated/root", "groovy/root", "jelly/root", "whitelist/root"}; + for (String url : urls) { + HtmlPage root = j.createWebClient().goTo(url); + assertEquals("Fragment", root.getElementById("frag").asText()); + } + } + + @Test + public void canViewIndex() throws Exception { + String[] urls = {"annotated", "groovy", "jelly"}; + for (String url : urls) { + HtmlPage root = j.createWebClient().goTo(url); + assertEquals("Fragment", root.getElementById("frag").asText()); + } + } + + @Test + public void canViewPagesThatIncludeViews() throws Exception { + String[] urls = {"groovy/include", "jelly/include"}; + for (String url : urls) { + HtmlPage root = j.createWebClient().goTo(url); + assertEquals("Fragment", root.getElementById("frag").asText()); + } + } + + @Test + public void canViewPagesThatRedirectToViews() throws Exception { + String[] urls = {"groovy/redirect", "jelly/redirect"}; + for (String url : urls) { + HtmlPage root = j.createWebClient().goTo(url); + assertEquals("Fragment", root.getElementById("frag").asText()); + } + } + + @Test + public void canViewCompressedViews() throws Exception { + String[] urls = {"groovy/compress", "jelly/compress"}; + for (String url : urls) { + HtmlPage root = j.createWebClient().goTo(url); + assertEquals("Fragment", root.getElementById("frag").asText()); + } + } + + @Test + public void cannotViewFragment() throws Exception { + String[] urls = {"annotated/frag", "groovy/frag", "jelly/frag", "whitelist/frag"}; + for (String url : urls) { + j.createWebClient().assertFails(url, HttpURLConnection.HTTP_NOT_FOUND); + } + } + + @Test + public void canSetStatusCodeBeforeValidation() throws Exception { + String[] urls = {"groovy/error", "jelly/error"}; + for (String url : urls) { + j.createWebClient().assertFails(url, 400); + } + } + + private static class Base implements UnprotectedRootAction { + @CheckForNull + @Override + public String getIconFileName() { + return null; + } + + @CheckForNull + @Override + public String getDisplayName() { + return getClass().getSimpleName() + " Test Data"; + } + + @CheckForNull + @Override + public String getUrlName() { + return getClass().getSimpleName().toLowerCase(Locale.ENGLISH); + } + } + + @TestExtension + public static class Jelly extends Base { + } + + @TestExtension + public static class Groovy extends Base { + } + + @TestExtension + @StaplerViews("explicitRoot") + @StaplerFragments("explicitFrag") + public static class Annotated extends Base { + } + + @TestExtension + public static class Whitelist extends Base { + } + + @TestExtension + public static class Extended extends Annotated { + } +}
test/src/test/resources/hudson/core/PluginManagerOverrideTest/BasicPluginManagerOverride/newview.jelly+4 −3 modified@@ -1,6 +1,7 @@ <!-- Trivial dummy view with a dummy element to demonstrate adding view --> <?jelly escape-by-default='true'?> -<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> - <st:contentType value="text/html;charset=UTF-8" /> - <div style="margin-top:1em" id="dummyElement">LoremIpsum</div> +<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout"> + <l:view contentType="text/html;charset=UTF-8"> + <div style="margin-top:1em" id="dummyElement">LoremIpsum</div> + </l:view> </j:jelly> \ No newline at end of file
test/src/test/resources/hudson/HtmlEscapeTest/foo.jelly+16 −15 modified@@ -1,17 +1,18 @@ <?jelly escape-by-default='true'?> -<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:i="jelly:fmt"> - <st:contentType value="text/html;charset=UTF-8"/> - <html> - <body> - <div id="d1"> - ${%msg} - </div> - <div id="d2"> - ${%msg2(it.str)} - </div> - <div id="d3"> - ${it.str} - </div> - </body> - </html> +<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout"> + <l:view contentType="text/html;charset=UTF-8"> + <html> + <body> + <div id="d1"> + ${%msg} + </div> + <div id="d2"> + ${%msg2(it.str)} + </div> + <div id="d3"> + ${it.str} + </div> + </body> + </html> + </l:view> </j:jelly> \ No newline at end of file
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Annotated/explicitFrag.jelly+27 −0 added@@ -0,0 +1,27 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core"> + <p id="explicit-frag">Explicit Fragment</p> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Annotated/explicitRoot.jelly+29 −0 added@@ -0,0 +1,29 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"> + <st:contentType value="text/html"/> + <st:include page="frag"/> + <st:include page="explicitFrag"/> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Annotated/frag.jelly+27 −0 added@@ -0,0 +1,27 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core"> + <p id="frag">Fragment</p> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Annotated/index.jelly+28 −0 added@@ -0,0 +1,28 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"> + <st:contentType value="text/html"/> + <p id="frag">Fragment</p> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Annotated/root.jelly+29 −0 added@@ -0,0 +1,29 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout"> + <l:layout title="Root Page"> + <st:include page="frag.jelly"/> + </l:layout> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Extended/whitelistedRoot.jelly+29 −0 added@@ -0,0 +1,29 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"> + <st:contentType value="text/html"/> + <st:include page="frag"/> + <st:include page="explicitFrag"/> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Groovy/compress.groovy+34 −0 added@@ -0,0 +1,34 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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.StaplerDispatchValidatorTest.Groovy + +def st = namespace('jelly:stapler') +def l = namespace('/lib/layout') +st.compress { + l.view { + st.contentType(value: 'text/html') + st.include(page: 'frag') + } +}
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Groovy/error.groovy+27 −0 added@@ -0,0 +1,27 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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.StaplerDispatchValidatorTest.Groovy + +namespace('jelly:stapler').statusCode(value: 400)
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Groovy/frag.groovy+28 −0 added@@ -0,0 +1,28 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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.StaplerDispatchValidatorTest.Groovy +p(id: 'frag') { + text('Fragment') +}
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Groovy/include.groovy+27 −0 added@@ -0,0 +1,27 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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.StaplerDispatchValidatorTest.Groovy + +namespace('jelly:stapler').include(page: 'root')
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Groovy/index.groovy+29 −0 added@@ -0,0 +1,29 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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.StaplerDispatchValidatorTest.Groovy +namespace('jelly:stapler').contentType(value: 'text/html') +p(id: 'frag') { + text('Fragment') +}
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Groovy/redirect.groovy+27 −0 added@@ -0,0 +1,27 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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.StaplerDispatchValidatorTest.Groovy + +namespace('jelly:stapler').redirect(url: 'root')
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Groovy/root.groovy+32 −0 added@@ -0,0 +1,32 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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.StaplerDispatchValidatorTest.Groovy + +def st = namespace('jelly:stapler') +def l = namespace('/lib/layout') +l.view { + st.contentType(value: 'text/html') + st.include(page: 'frag') +}
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Jelly/compress.jelly+31 −0 added@@ -0,0 +1,31 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout"> + <st:compress> + <l:view contentType="text/html;charset=UTF-8"> + <st:include page="frag"/> + </l:view> + </st:compress> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Jelly/error.jelly+27 −0 added@@ -0,0 +1,27 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"> + <st:statusCode value="400"/> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Jelly/frag.jelly+27 −0 added@@ -0,0 +1,27 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core"> + <p id="frag">Fragment</p> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Jelly/include.jelly+27 −0 added@@ -0,0 +1,27 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"> + <st:include page="root"/> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Jelly/index.jelly+28 −0 added@@ -0,0 +1,28 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"> + <st:contentType value="text/html"/> + <p id="frag">Fragment</p> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Jelly/redirect.jelly+27 −0 added@@ -0,0 +1,27 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"> + <st:redirect url="root"/> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Jelly/root.jelly+29 −0 added@@ -0,0 +1,29 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout"> + <l:view contentType="text/html;charset=UTF-8"> + <st:include page="frag"/> + </l:view> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Whitelist/frag.jelly+27 −0 added@@ -0,0 +1,27 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> +<j:jelly xmlns:j="jelly:core"> + <p id="frag">Fragment</p> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Whitelist/root.jelly+29 −0 added@@ -0,0 +1,29 @@ +<?jelly escape-by-default="true"?> +<!-- + ~ The MIT License + ~ + ~ Copyright (c) 2019 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. + --> + +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"> + <st:contentType value="text/html"/> + <st:include page="frag"/> +</j:jelly> \ No newline at end of file
test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/whitelist.txt+2 −0 added@@ -0,0 +1,2 @@ +jenkins.security.stapler.StaplerDispatchValidatorTest$Whitelist root !frag +jenkins.security.stapler.StaplerDispatchValidatorTest$Extended whitelistedRoot
test/src/test/resources/lib/layout/RenderOnDemandTest/externalScript.jelly+2 −2 modified@@ -1,4 +1,4 @@ <?jelly escape-by-default='true'?> -<j:jelly xmlns:j="jelly:core"> +<l:view xmlns:l="/lib/layout" contentType="text/javascript"> y = "yyy"; -</j:jelly> +</l:view>
16 files changed · +571 −158
core/src/main/java/org/kohsuke/stapler/DispatchValidator.java+97 −0 added@@ -0,0 +1,97 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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 org.kohsuke.stapler; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * Validates dispatch requests. This validator is configured through {@link WebApp#setDispatchValidator(DispatchValidator)} + * and is automatically used by dispatchers created through {@link Facet#createValidatingDispatcher(AbstractTearOff, ScriptExecutor)}. + * Extends {@linkplain Facet#createValidatingDispatcher(AbstractTearOff, ScriptExecutor) facet dispatchers} to provide validation + * of views before they dispatch, thus allowing a final veto before dispatch begins writing any response body to the + * client. + * + * @see WebApp#setDispatchValidator(DispatchValidator) + * @since TODO + */ +public interface DispatchValidator { + + /** + * Checks if the given request and response should be allowed to dispatch. Returns {@code null} to indicate an + * unknown or neutral result. + * + * @param req the HTTP request to validate + * @param rsp the HTTP response + * @return true if the request should be dispatched, false if not, or null if unknown or neutral + */ + @CheckForNull Boolean isDispatchAllowed(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp); + + /** + * Checks if the given request and response should be allowed to dispatch a view on an optionally present node + * object. Returns {@code null} to indicate an unknown or neutral result. + * + * @param req the HTTP request to validate + * @param rsp the HTTP response + * @param viewName the name of the view to dispatch + * @param node the node being dispatched if present + * @return true if the view should be allowed to dispatch, false if it should not, or null if unknown + */ + default @CheckForNull Boolean isDispatchAllowed(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp, @Nonnull String viewName, @CheckForNull Object node) { + return isDispatchAllowed(req, rsp); + } + + /** + * Allows the given request to be dispatched. Further calls to {@link #isDispatchAllowed(StaplerRequest, StaplerResponse)} + * should return true for the same request. + */ + void allowDispatch(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp); + + /** + * Throws a {@link CancelRequestHandlingException} if the given request is not + * {@linkplain #isDispatchAllowed(StaplerRequest, StaplerResponse) allowed}. + */ + default void requireDispatchAllowed(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp) throws CancelRequestHandlingException { + Boolean allowed = isDispatchAllowed(req, rsp); + if (allowed == null || !allowed) { + throw new CancelRequestHandlingException(); + } + } + + /** + * Default validator implementation that explicitly allows all dispatch requests to proceed. + */ + DispatchValidator DEFAULT = new DispatchValidator() { + @Override + public Boolean isDispatchAllowed(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp) { + return true; + } + + @Override + public void allowDispatch(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp) { + // no-op + } + }; +}
core/src/main/java/org/kohsuke/stapler/event/FilteredDispatchTriggerListener.java+50 −0 added@@ -0,0 +1,50 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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 org.kohsuke.stapler.event; + +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import java.util.logging.Logger; + +/** + * Listens to filtered dispatch events from {@link org.kohsuke.stapler.DispatchValidator}. + * + * @since TODO + * @see org.kohsuke.stapler.WebApp#setFilteredDispatchTriggerListener(FilteredDispatchTriggerListener) + */ +public interface FilteredDispatchTriggerListener { + boolean onDispatchTrigger(StaplerRequest req, StaplerResponse rsp, Object node, String viewName); + + FilteredDispatchTriggerListener JUST_WARN = new FilteredDispatchTriggerListener() { + private final Logger LOGGER = Logger.getLogger(FilteredDispatchTriggerListener.class.getName()); + + @Override + public boolean onDispatchTrigger(StaplerRequest req, StaplerResponse rsp, Object node, String viewName) { + LOGGER.warning(() -> "BLOCKED -> <" + node + ">." + viewName); + return false; + } + }; +}
core/src/main/java/org/kohsuke/stapler/Facet.java+141 −8 modified@@ -27,8 +27,11 @@ import org.apache.commons.discovery.resource.ClassLoaders; import org.apache.commons.discovery.resource.names.DiscoverServiceNames; import org.kohsuke.MetaInfServices; +import org.kohsuke.stapler.event.FilteredDispatchTriggerListener; import org.kohsuke.stapler.lang.Klass; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import java.io.IOException; @@ -50,6 +53,7 @@ public abstract class Facet { /** * Adds {@link Dispatcher}s that look at one token and binds that * to the views associated with the 'it' object. + * @see #createValidatingDispatcher(AbstractTearOff, ScriptExecutor) */ public abstract void buildViewDispatchers(MetaClass owner, List<Dispatcher> dispatchers); @@ -89,18 +93,16 @@ public static <T> List<T> discoverExtensions(Class<T> type, ClassLoader... cls) String name = itr.nextResourceName(); if (!classNames.add(name)) continue; // avoid duplication - Class<?> c; + Class<? extends T> c; try { - c = cl.loadClass(name); + c = cl.loadClass(name).asSubclass(type); } catch (ClassNotFoundException e) { LOGGER.log(Level.WARNING, "Failed to load "+name,e); continue; } try { - r.add((T)c.newInstance()); - } catch (InstantiationException e) { - LOGGER.log(Level.WARNING, "Failed to instantiate "+c,e); - } catch (IllegalAccessException e) { + r.add(c.newInstance()); + } catch (InstantiationException | IllegalAccessException e) { LOGGER.log(Level.WARNING, "Failed to instantiate "+c,e); } } @@ -119,12 +121,13 @@ public static <T> List<T> discoverExtensions(Class<T> type, ClassLoader... cls) * @param type * If "it" is non-null, {@code it.getClass()}. Otherwise the class * from which the view is searched. + * @see #createRequestDispatcher(AbstractTearOff, ScriptExecutor, Object, String) */ - public RequestDispatcher createRequestDispatcher(RequestImpl request, Klass<?> type, Object it, String viewName) throws IOException { + @CheckForNull public RequestDispatcher createRequestDispatcher(RequestImpl request, Klass<?> type, Object it, String viewName) throws IOException { return null; // should be really abstract, but not } - public RequestDispatcher createRequestDispatcher(RequestImpl request, Class type, Object it, String viewName) throws IOException { + @CheckForNull public RequestDispatcher createRequestDispatcher(RequestImpl request, Class type, Object it, String viewName) throws IOException { return createRequestDispatcher(request,Klass.java(type),it,viewName); } @@ -133,6 +136,7 @@ public RequestDispatcher createRequestDispatcher(RequestImpl request, Class type * * @return * true if the processing succeeds. Otherwise false. + * @see #handleIndexRequest(AbstractTearOff, ScriptExecutor, RequestImpl, ResponseImpl, Object) */ public abstract boolean handleIndexRequest(RequestImpl req, ResponseImpl rsp, Object node, MetaClass nodeMetaClass) throws IOException, ServletException; @@ -162,4 +166,133 @@ protected boolean isBasename(String potentialPath){ return true; } } + + /** + * Creates a Dispatcher that integrates {@link DispatchValidator} with the provided script loader and executor. + * If an exception or one of its causes is a {@link CancelRequestHandlingException}, this will cause the + * Dispatcher to cancel and return false, thus allowing for further dispatchers to attempt to handle the request. + * This also requires validation to pass before any output can be written to the response. + * In any error case, the configured {@link FilteredDispatchTriggerListener} will be notified. + * + * @param scriptLoader tear-off script loader to find views + * @param scriptExecutor script executor for rendering views + * @param <S> type of script + * @return dispatcher that handles scripts + * @see WebApp#setDispatchValidator(DispatchValidator) + * @see WebApp#setFilteredDispatchTriggerListener(FilteredDispatchTriggerListener) + * @since TODO + */ + @Nonnull protected <S> Dispatcher createValidatingDispatcher(@Nonnull AbstractTearOff<?, ? extends S, ?> scriptLoader, + @Nonnull ScriptExecutor<? super S> scriptExecutor) { + return new Dispatcher() { + @Override + public boolean dispatch(@Nonnull RequestImpl req, @Nonnull ResponseImpl rsp, @CheckForNull Object node) throws ServletException { + String next = req.tokens.peek(); + if (next == null) { + return false; + } + // only match end of URL + if (req.tokens.countRemainingTokens() > 1) { + return false; + } + // avoid serving both foo and foo/ as they have different URL semantics + if (req.tokens.endsWithSlash) { + return false; + } + // prevent potential path traversal + if (!isBasename(next)) { + return false; + } + DispatchValidator validator = req.getWebApp().getDispatchValidator(); + FilteredDispatchTriggerListener listener = req.getWebApp().getFilteredDispatchTriggerListener(); + Boolean valid = validator.isDispatchAllowed(req, rsp, next, node); + if (valid != null && !valid) { + return listener.onDispatchTrigger(req, rsp, node, next); + } + S script; + try { + script = scriptLoader.findScript(next); + } catch (Exception e) { + throw new ServletException(e); + } + if (script == null) { + return false; + } + req.tokens.next(); + anonymizedTraceEval(req, rsp, node, "%s: View: %s%s", next, scriptLoader.getDefaultScriptExtension()); + if (traceable()) { + trace(req, rsp, "-> %s on <%s>", next, node); + } + try { + scriptExecutor.execute(req, rsp, script, node); + return true; + } catch (Exception e) { + req.tokens.prev(); + for (Throwable cause = e; cause != null; cause = cause.getCause()) { + if (cause instanceof CancelRequestHandlingException) { + return listener.onDispatchTrigger(req, rsp, node, next); + } + } + throw new ServletException(e); + } + } + + @Override + public String toString() { + return "VIEW" + scriptLoader.getDefaultScriptExtension() + " for url=/VIEW"; + } + }; + } + + /** + * Handles an index request by dispatching a script. + * @since TODO + */ + protected <S> boolean handleIndexRequest(@Nonnull AbstractTearOff<?, ? extends S, ?> scriptLoader, + @Nonnull ScriptExecutor<? super S> scriptExecutor, + @Nonnull RequestImpl req, + @Nonnull ResponseImpl rsp, + @CheckForNull Object node) + throws ServletException, IOException { + S script; + try { + script = scriptLoader.findScript("index"); + } catch (Exception e) { + throw new ServletException(e); + } + if (script == null) { + return false; + } + Dispatcher.anonymizedTraceEval(req, rsp, node, "Index: index%s", scriptLoader.getDefaultScriptExtension()); + if (Dispatcher.traceable()) { + Dispatcher.trace(req, rsp, "-> index on <%s>", node); + } + try { + scriptExecutor.execute(req, rsp, script, node); + return true; + } catch (Exception e) { + throw new ServletException(e); + } + } + + /** + * Creates a RequestDispatcher that integrates with {@link DispatchValidator} and + * {@link FilteredDispatchTriggerListener}. + * + * @param scriptLoader tear-off script loader for finding views + * @param scriptExecutor script executor for rendering views + * @param it the model node being dispatched against + * @param viewName name of the view to load and execute + * @param <S> view type + * @return a RequestDispatcher that performs similar validation to {@link #createValidatingDispatcher(AbstractTearOff, ScriptExecutor)} + * @see WebApp#setDispatchValidator(DispatchValidator) + * @see WebApp#setFilteredDispatchTriggerListener(FilteredDispatchTriggerListener) + * @since TODO + */ + @CheckForNull protected <S> RequestDispatcher createRequestDispatcher(@Nonnull AbstractTearOff<?, ? extends S, ?> scriptLoader, + @Nonnull ScriptExecutor<? super S> scriptExecutor, + @CheckForNull Object it, + @Nonnull String viewName) { + return ScriptRequestDispatcher.newRequestDispatcher(scriptLoader, scriptExecutor, viewName, it); + } }
core/src/main/java/org/kohsuke/stapler/IndexViewDispatcher.java+2 −0 modified@@ -27,6 +27,8 @@ public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws I if (req.tokens.hasMore()) return false; + // always allow index views to be dispatched + req.getWebApp().getDispatchValidator().allowDispatch(req, rsp); return facet.handleIndexRequest(req, rsp, node, metaClass); }
core/src/main/java/org/kohsuke/stapler/ScriptExecutor.java+42 −0 added@@ -0,0 +1,42 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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 org.kohsuke.stapler; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * Execution strategy for handling views written in other scripting languages. + * + * @param <S> script type + * @since TODO + */ +public interface ScriptExecutor<S> { + + /** + * Executes the given script on the given node and request, rendering output to the given response. + */ + void execute(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp, @Nonnull S script, @CheckForNull Object it) throws Exception; +}
core/src/main/java/org/kohsuke/stapler/ScriptRequestDispatcher.java+108 −0 added@@ -0,0 +1,108 @@ +/* + * The MIT License + * + * Copyright (c) 2019 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 org.kohsuke.stapler; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implements a RequestDispatcher for a given model node and view. Unlike dispatchers created through + * {@link Facet#createValidatingDispatcher(AbstractTearOff, ScriptExecutor)}, these dispatchers always allow scripts + * to be dispatched. + * @param <S> script view type + * @since TODO + */ +class ScriptRequestDispatcher<S> implements RequestDispatcher { + + private static final Logger LOGGER = Logger.getLogger(ScriptRequestDispatcher.class.getName()); + + @CheckForNull static <S> ScriptRequestDispatcher<S> newRequestDispatcher(@Nonnull AbstractTearOff<?, ? extends S, ?> scriptLoader, + @Nonnull ScriptExecutor<? super S> scriptExecutor, + @Nonnull String viewName, + @CheckForNull Object node) { + S script; + try { + script = scriptLoader.findScript(viewName); + } catch (Exception e) { + LOGGER.log(Level.WARNING, e, () -> "Could not load requested view " + viewName + " on model class " + (node == null ? null : node.getClass().getName())); + return null; + } + if (script == null) { + return null; + } + return new ScriptRequestDispatcher<>(scriptLoader.getDefaultScriptExtension(), scriptExecutor, viewName, script, node); + } + + private final @Nonnull String defaultScriptExtension; + private final @Nonnull ScriptExecutor<? super S> scriptExecutor; + private final @Nonnull String viewName; + private final @Nonnull S script; + private final @CheckForNull Object node; + + private ScriptRequestDispatcher(@Nonnull String defaultScriptExtension, + @Nonnull ScriptExecutor<? super S> scriptExecutor, + @Nonnull String viewName, + @Nonnull S script, + @CheckForNull Object node) { + this.defaultScriptExtension = defaultScriptExtension; + this.scriptExecutor = scriptExecutor; + this.viewName = viewName; + this.script = script; + this.node = node; + } + + + @Override + public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException { + StaplerRequest req = (StaplerRequest) request; + StaplerResponse rsp = (StaplerResponse) response; + DispatchValidator validator = req.getWebApp().getDispatchValidator(); + validator.allowDispatch(req, rsp); + try { + Dispatcher.anonymizedTraceEval(req, rsp, node, "%s: View: %s%s", viewName, defaultScriptExtension); + if (Dispatcher.traceable()) { + Dispatcher.trace(req, rsp, "-> %s on <%s>", viewName, node); + } + scriptExecutor.execute(req, rsp, script, node); + } catch (ServletException | IOException | RuntimeException e) { + throw e; + } catch (Exception e) { + throw new ServletException(e); + } + + } + + @Override + public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException { + forward(request, response); + } +}
core/src/main/java/org/kohsuke/stapler/WebApp.java+34 −2 modified@@ -28,6 +28,7 @@ import org.kohsuke.stapler.event.FilteredDoActionTriggerListener; import org.kohsuke.stapler.event.FilteredFieldTriggerListener; import org.kohsuke.stapler.event.FilteredGetterTriggerListener; +import org.kohsuke.stapler.event.FilteredDispatchTriggerListener; import org.kohsuke.stapler.lang.FieldRef; import org.kohsuke.stapler.lang.KInstance; import org.kohsuke.stapler.lang.Klass; @@ -145,12 +146,15 @@ public static WebApp get(ServletContext context) { private FunctionList.Filter filterForGetMethods = FunctionList.Filter.ALWAYS_OK; private FunctionList.Filter filterForDoActions = FunctionList.Filter.ALWAYS_OK; private FieldRef.Filter filterForFields = FieldRef.Filter.ALWAYS_OK; - + private DispatchersFilter dispatchersFilter; private FilteredDoActionTriggerListener filteredDoActionTriggerListener = FilteredDoActionTriggerListener.JUST_WARN; private FilteredGetterTriggerListener filteredGetterTriggerListener = FilteredGetterTriggerListener.JUST_WARN; private FilteredFieldTriggerListener filteredFieldTriggerListener = FilteredFieldTriggerListener.JUST_WARN; - + + private DispatchValidator dispatchValidator = DispatchValidator.DEFAULT; + private FilteredDispatchTriggerListener filteredDispatchTriggerListener = FilteredDispatchTriggerListener.JUST_WARN; + /** * Give the application a way to customize the JSON before putting it inside Stacktrace when something wrong happened. * By default it just returns the given JSON. @@ -412,4 +416,32 @@ public JsonInErrorMessageSanitizer getJsonInErrorMessageSanitizer() { public void setJsonInErrorMessageSanitizer(JsonInErrorMessageSanitizer jsonInErrorMessageSanitizer) { this.jsonInErrorMessageSanitizer = jsonInErrorMessageSanitizer; } + + public DispatchValidator getDispatchValidator() { + if (dispatchValidator == null) { + dispatchValidator = DispatchValidator.DEFAULT; + } + return dispatchValidator; + } + + /** + * Sets the validator used with facet dispatchers. + */ + public void setDispatchValidator(DispatchValidator dispatchValidator) { + this.dispatchValidator = dispatchValidator; + } + + public FilteredDispatchTriggerListener getFilteredDispatchTriggerListener() { + if (filteredDispatchTriggerListener == null) { + filteredDispatchTriggerListener = FilteredDispatchTriggerListener.JUST_WARN; + } + return filteredDispatchTriggerListener; + } + + /** + * Sets the event listener used for reacting to filtered dispatch requests. + */ + public void setFilteredDispatchTriggerListener(FilteredDispatchTriggerListener filteredDispatchTriggerListener) { + this.filteredDispatchTriggerListener = filteredDispatchTriggerListener; + } }
.gitignore+2 −1 modified@@ -3,7 +3,8 @@ *.iml *.iws target +pom.xml.versionsBackup .project .settings/ -.classpath \ No newline at end of file +.classpath
groovy/src/main/java/org/kohsuke/stapler/jelly/groovy/GroovyClassTearOff.java+1 −0 modified@@ -53,6 +53,7 @@ public GroovierJellyScript parseScript(URL res) throws IOException { /** * Creates a {@link RequestDispatcher} that forwards to the jelly view, if available. */ + @Deprecated public RequestDispatcher createDispatcher(Object it, String viewName) throws IOException { Script script = findScript(viewName); if(script!=null)
groovy/src/main/java/org/kohsuke/stapler/jelly/groovy/GroovyFacet.java+14 −65 modified@@ -24,17 +24,16 @@ package org.kohsuke.stapler.jelly.groovy; import org.apache.commons.jelly.JellyException; -import org.apache.commons.jelly.Script; import org.kohsuke.MetaInfServices; import org.kohsuke.stapler.Dispatcher; import org.kohsuke.stapler.Facet; import org.kohsuke.stapler.MetaClass; import org.kohsuke.stapler.RequestImpl; import org.kohsuke.stapler.ResponseImpl; -import org.kohsuke.stapler.WebApp; import org.kohsuke.stapler.jelly.JellyClassTearOff; import org.kohsuke.stapler.jelly.JellyCompatibleFacet; import org.kohsuke.stapler.jelly.JellyFacet; +import org.kohsuke.stapler.jelly.ScriptInvoker; import org.kohsuke.stapler.lang.Klass; import javax.servlet.RequestDispatcher; @@ -53,55 +52,11 @@ */ @MetaInfServices(Facet.class) public class GroovyFacet extends Facet implements JellyCompatibleFacet { - public void buildViewDispatchers(final MetaClass owner, List<Dispatcher> dispatchers) { - dispatchers.add(new Dispatcher() { - final GroovyClassTearOff tearOff = owner.loadTearOff(GroovyClassTearOff.class); - final GroovyServerPageTearOff gsp = owner.loadTearOff(GroovyServerPageTearOff.class); - - public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException { - // check Groovy view - String next = req.tokens.peek(); - if(next==null) return false; - - // only match the end of the URL - if (req.tokens.countRemainingTokens()>1) return false; - // and avoid serving both "foo" and "foo/" as relative URL semantics are drastically different - if (req.getRequestURI().endsWith("/")) return false; - - if (!isBasename(next)) { - // potentially an attempt to make a folder traversal - return false; - } - - try { - Script script = tearOff.findScript(next); - if(script==null) - script = gsp.findScript(next); - if (script==null) - return false; // no Groovy script found - - req.tokens.next(); - - Dispatcher.anonymizedTraceEval(req, rsp, node, "%s: Groovy facet: %s", next + ".groovy"); - if(traceable()) - trace(req,rsp,"Invoking "+next+".groovy"+" on "+node+" for "+req.tokens); - - WebApp.getCurrent().getFacet(JellyFacet.class).scriptInvoker.invokeScript(req, rsp, script, node); - return true; - } catch (RuntimeException e) { - throw e; - } catch (IOException e) { - throw e; - } catch (Exception e) { - throw new ServletException(e); - } - } - public String toString() { - // or TOKEN.gsp - return "TOKEN.groovy for url=/TOKEN"; - } - }); + public void buildViewDispatchers(final MetaClass owner, List<Dispatcher> dispatchers) { + ScriptInvoker scriptInvoker = owner.webApp.getFacet(JellyFacet.class).scriptInvoker; + dispatchers.add(createValidatingDispatcher(owner.loadTearOff(GroovyClassTearOff.class), scriptInvoker)); + dispatchers.add(createValidatingDispatcher(owner.loadTearOff(GroovyServerPageTearOff.class), scriptInvoker)); } public Collection<Class<GroovyClassTearOff>> getClassTearOffTypes() { @@ -113,14 +68,12 @@ public Collection<String> getScriptExtensions() { } public RequestDispatcher createRequestDispatcher(RequestImpl request, Klass type, Object it, String viewName) throws IOException { - MetaClass mc = request.stapler.getWebApp().getMetaClass(type); - return createDispatcher(it, viewName, mc); - } - - private RequestDispatcher createDispatcher(Object it, String viewName, MetaClass mc) throws IOException { - RequestDispatcher d = mc.loadTearOff(GroovyClassTearOff.class).createDispatcher(it, viewName); - if (d==null) - d = mc.loadTearOff(GroovyServerPageTearOff.class).createDispatcher(it, viewName); + MetaClass owner = request.getWebApp().getMetaClass(type); + ScriptInvoker scriptExecutor = request.getWebApp().getFacet(JellyFacet.class).scriptInvoker; + RequestDispatcher d = createRequestDispatcher(owner.loadTearOff(GroovyClassTearOff.class), scriptExecutor, it, viewName); + if (d == null) { + d = createRequestDispatcher(owner.loadTearOff(GroovyServerPageTearOff.class), scriptExecutor, it, viewName); + } return d; } @@ -136,13 +89,9 @@ public void buildIndexDispatchers(MetaClass owner, List<Dispatcher> dispatchers) } public boolean handleIndexRequest(RequestImpl req, ResponseImpl rsp, Object node, MetaClass nodeMetaClass) throws IOException, ServletException { - RequestDispatcher d = createDispatcher(node,"index", nodeMetaClass); - - if (d!=null) { - d.forward(req, rsp); - return true; - } - return false; + ScriptInvoker scriptExecutor = req.getWebApp().getFacet(JellyFacet.class).scriptInvoker; + return handleIndexRequest(nodeMetaClass.loadTearOff(GroovyClassTearOff.class), scriptExecutor, req, rsp, node) || + handleIndexRequest(nodeMetaClass.loadTearOff(GroovyServerPageTearOff.class), scriptExecutor, req, rsp, node); } private static final Set<Class<GroovyClassTearOff>> TEAROFF_TYPES = Collections.singleton(GroovyClassTearOff.class);
groovy/src/main/java/org/kohsuke/stapler/jelly/groovy/GroovyServerPageTearOff.java+1 −0 modified@@ -32,6 +32,7 @@ public GroovierJellyScript parseScript(URL res) throws IOException { /** * Creates a {@link RequestDispatcher} that forwards to the jelly view, if available. */ + @Deprecated public RequestDispatcher createDispatcher(Object it, String viewName) throws IOException { GroovierJellyScript s = findScript(viewName); if (s!=null) return new JellyRequestDispatcher(it,s);
jelly/src/main/java/org/kohsuke/stapler/jelly/DefaultScriptInvoker.java+60 −19 modified@@ -23,21 +23,21 @@ package org.kohsuke.stapler.jelly; -import org.kohsuke.stapler.Stapler; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; -import org.apache.commons.jelly.Script; +import org.apache.commons.jelly.JellyContext; import org.apache.commons.jelly.JellyTagException; +import org.apache.commons.jelly.Script; import org.apache.commons.jelly.XMLOutput; import org.apache.commons.jelly.XMLOutputFactory; -import org.apache.commons.jelly.JellyContext; import org.apache.commons.jelly.impl.TagScript; +import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import javax.annotation.Nonnull; import javax.servlet.ServletContext; +import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.io.BufferedOutputStream; -import java.io.FilterOutputStream; import java.io.Writer; import java.util.Enumeration; @@ -81,25 +81,66 @@ private boolean doCompression(Script script) { } return false; } + + private interface OutputStreamSupplier { + @Nonnull OutputStream get() throws IOException; + } + + private class LazyOutputStreamSupplier implements OutputStreamSupplier { + private final OutputStreamSupplier supplier; + private volatile OutputStream out; + + private LazyOutputStreamSupplier(OutputStreamSupplier supplier) { + this.supplier = supplier; + } + + @Override + @Nonnull + public OutputStream get() throws IOException { + if (out == null) { + synchronized (this) { + if (out == null) { + out = supplier.get(); + } + } + } + return out; + } + } + protected OutputStream createOutputStream(StaplerRequest req, StaplerResponse rsp, Script script, Object it) throws IOException { - // do we want to do compression? - OutputStream output=null; - if (doCompression(script)) - output = rsp.getCompressedOutputStream(req); - if(output==null) // nope - output = new BufferedOutputStream(rsp.getOutputStream()); - - output = new FilterOutputStream(output) { - public void flush() { + OutputStreamSupplier out = new LazyOutputStreamSupplier(() -> { + req.getWebApp().getDispatchValidator().requireDispatchAllowed(req, rsp); + return doCompression(script) ? rsp.getCompressedOutputStream(req) : new BufferedOutputStream(rsp.getOutputStream()); + }); + return new OutputStream() { + @Override + public void write(int b) throws IOException { + out.get().write(b); + } + + @Override + public void write(byte[] b) throws IOException { + out.get().write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.get().write(b, off, len); + } + + @Override + public void flush() throws IOException { // flushing ServletOutputStream causes Tomcat to // send out headers, making it impossible to set contentType from the script. // so don't let Jelly flush. } - public void write(byte b[], int off, int len) throws IOException { - out.write(b, off, len); + + @Override + public void close() throws IOException { + out.get().close(); } }; - return output; } protected void exportVariables(StaplerRequest req, StaplerResponse rsp, Script script, Object it, JellyContext context) {
jelly/src/main/java/org/kohsuke/stapler/jelly/JellyClassTearOff.java+5 −4 modified@@ -26,22 +26,21 @@ import org.apache.commons.jelly.JellyException; import org.apache.commons.jelly.Script; import org.kohsuke.stapler.AbstractTearOff; -import static org.kohsuke.stapler.Dispatcher.trace; -import static org.kohsuke.stapler.Dispatcher.traceable; - import org.kohsuke.stapler.Dispatcher; import org.kohsuke.stapler.Facet; import org.kohsuke.stapler.MetaClass; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.WebApp; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import java.io.IOException; import java.net.URL; import java.util.logging.Logger; +import static org.kohsuke.stapler.Dispatcher.trace; +import static org.kohsuke.stapler.Dispatcher.traceable; + /** * @author Kohsuke Kawaguchi */ @@ -98,6 +97,7 @@ public Script resolveScript(String name) throws JellyException { /** * Serves <tt>index.jelly</tt> if it's available, and returns true. */ + @Deprecated public boolean serveIndexJelly(StaplerRequest req, StaplerResponse rsp, Object node) throws ServletException, IOException { try { Script script = findScript("index.jelly"); @@ -123,6 +123,7 @@ public boolean serveIndexJelly(StaplerRequest req, StaplerResponse rsp, Object n /** * Creates a {@link RequestDispatcher} that forwards to the jelly view, if available. */ + @Deprecated public RequestDispatcher createDispatcher(Object it, String viewName) throws IOException { try { // backward compatible behavior that expects full file name including ".jelly"
jelly/src/main/java/org/kohsuke/stapler/jelly/JellyFacet.java+4 −57 modified@@ -24,15 +24,13 @@ package org.kohsuke.stapler.jelly; import org.apache.commons.jelly.JellyException; -import org.apache.commons.jelly.Script; import org.apache.commons.jelly.expression.ExpressionFactory; import org.kohsuke.MetaInfServices; import org.kohsuke.stapler.Dispatcher; import org.kohsuke.stapler.Facet; import org.kohsuke.stapler.MetaClass; import org.kohsuke.stapler.RequestImpl; import org.kohsuke.stapler.ResponseImpl; -import org.kohsuke.stapler.TearOffSupport; import org.kohsuke.stapler.lang.Klass; import javax.servlet.RequestDispatcher; @@ -64,58 +62,7 @@ public class JellyFacet extends Facet implements JellyCompatibleFacet { public volatile ResourceBundleFactory resourceBundleFactory = ResourceBundleFactory.INSTANCE; public void buildViewDispatchers(final MetaClass owner, List<Dispatcher> dispatchers) { - dispatchers.add(new Dispatcher() { - final JellyClassTearOff tearOff = owner.loadTearOff(JellyClassTearOff.class); - - public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException { - // check Jelly view - String next = req.tokens.peek(); - if(next==null) return false; - - // only match the end of the URL - if (req.tokens.countRemainingTokens()>1) return false; - // and avoid serving both "foo" and "foo/" as relative URL semantics are drastically different - if (req.tokens.endsWithSlash) return false; - - if (!isBasename(next)) { - // potentially an attempt to make a folder traversal - return false; - } - - try { - Script script = tearOff.findScript(next+".jelly"); - - if(script==null) return false; // no Jelly script found - - req.tokens.next(); - - // Null not expected here - String src = next+".jelly"; - if (script instanceof JellyViewScript) { - JellyViewScript jvs = (JellyViewScript) script; - src = jvs.getName(); - } - Dispatcher.anonymizedTraceEval(req, rsp, node, "%s: Jelly facet: %s", src); - if (traceable()) { - trace(req,rsp,"-> %s on <%s>", src, node); - } - - scriptInvoker.invokeScript(req, rsp, script, node); - - return true; - } catch (RuntimeException e) { - throw e; - } catch (IOException e) { - throw e; - } catch (Exception e) { - throw new ServletException(e); - } - } - - public String toString() { - return "VIEW.jelly for url=/VIEW"; - } - }); + dispatchers.add(createValidatingDispatcher(owner.loadTearOff(JellyClassTearOff.class), scriptInvoker)); } @Override @@ -138,12 +85,12 @@ public Collection<String> getScriptExtensions() { } public RequestDispatcher createRequestDispatcher(RequestImpl request, Klass<?> type, Object it, String viewName) throws IOException { - TearOffSupport mc = request.stapler.getWebApp().getMetaClass(type); - return mc.loadTearOff(JellyClassTearOff.class).createDispatcher(it,viewName); + JellyClassTearOff scriptLoader = request.getWebApp().getMetaClass(type).loadTearOff(JellyClassTearOff.class); + return createRequestDispatcher(scriptLoader, scriptInvoker, it, viewName); } public boolean handleIndexRequest(RequestImpl req, ResponseImpl rsp, Object node, MetaClass nodeMetaClass) throws IOException, ServletException { - return nodeMetaClass.loadTearOff(JellyClassTearOff.class).serveIndexJelly(req,rsp,node); + return handleIndexRequest(nodeMetaClass.loadTearOff(JellyClassTearOff.class), scriptInvoker, req, rsp, node); } /**
jelly/src/main/java/org/kohsuke/stapler/jelly/ScriptInvoker.java+9 −1 modified@@ -24,11 +24,14 @@ package org.kohsuke.stapler.jelly; import org.apache.commons.jelly.XMLOutput; +import org.kohsuke.stapler.ScriptExecutor; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.apache.commons.jelly.JellyTagException; import org.apache.commons.jelly.Script; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import java.io.IOException; /** @@ -37,7 +40,7 @@ * @author Kohsuke Kawaguchi * @see JellyFacet#scriptInvoker */ -public interface ScriptInvoker { +public interface ScriptInvoker extends ScriptExecutor<Script> { /** * Invokes the script and generates output to {@link StaplerResponse#getOutputStream()}. */ @@ -47,4 +50,9 @@ public interface ScriptInvoker { * Invokes the script and generates output to the specified output */ void invokeScript(StaplerRequest req, StaplerResponse rsp, Script script, Object it, XMLOutput out) throws IOException, JellyTagException; + + @Override + default void execute(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp, @Nonnull Script script, @CheckForNull Object it) throws IOException, JellyTagException { + invokeScript(req, rsp, script, it); + } }
pom.xml+1 −1 modified@@ -81,7 +81,7 @@ <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.level>8</java.level> - <revision>1.258</revision> + <revision>1.257.1</revision> <changelist>-SNAPSHOT</changelist> <incrementals.url>https://repo.jenkins-ci.org/incrementals/</incrementals.url> <scmTag>HEAD</scmTag>
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- access.redhat.com/errata/RHSA-2019:2503ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2019:2548ghsavendor-advisoryx_refsource_REDHATWEB
- github.com/advisories/GHSA-6jfc-mc97-c7wgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2019-10354ghsaADVISORY
- www.openwall.com/lists/oss-security/2019/07/17/2ghsamailing-listx_refsource_MLISTWEB
- www.securityfocus.com/bid/109373mitrevdb-entryx_refsource_BID
- github.com/jenkinsci/jenkins/commit/279d8109eddb7a494428baf25af9756c2e33576bghsaWEB
- github.com/jenkinsci/stapler/commit/19637555a9f32d3875356b47234131d8b1e9fee4ghsaWEB
- jenkins.io/security/advisory/2019-07-17/ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.