-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
343 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
225 changes: 225 additions & 0 deletions
225
src/main/java/org/jenkinsci/plugins/workflow/cps/GroovySourceFileAllowlist.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
/* | ||
* The MIT License | ||
* | ||
* Copyright 2022 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.jenkinsci.plugins.workflow.cps; | ||
|
||
import edu.umd.cs.findbugs.annotations.CheckForNull; | ||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | ||
import hudson.Extension; | ||
import hudson.ExtensionList; | ||
import hudson.ExtensionPoint; | ||
import hudson.Main; | ||
import java.io.BufferedReader; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.InputStreamReader; | ||
import java.net.URL; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.Enumeration; | ||
import java.util.List; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
import jenkins.util.SystemProperties; | ||
import org.apache.commons.lang.StringUtils; | ||
|
||
/** | ||
* Determines what Groovy source files can be loaded in Pipelines. | ||
* | ||
* In Pipeline, the standard behavior of {@code GroovyClassLoader} would allow Groovy source files from core or plugins | ||
* to be loaded as long as they are somewhere on the classpath. This includes things like Groovy views, which are not | ||
* intended to be available to pipelines. When these files are loaded, they are loaded by the trusted | ||
* {@link CpsGroovyShell} and are not sandbox-transformed, which means that allowing arbitrary Groovy source files to | ||
* be loaded is potentially unsafe. | ||
* | ||
* {@link ClassLoaderImpl} blocks all Groovy source files from being loaded by default unless they are allowed by an | ||
* implementation of this extension point. | ||
*/ | ||
public abstract class GroovySourceFileAllowlist implements ExtensionPoint { | ||
private static final Logger LOGGER = Logger.getLogger(GroovySourceFileAllowlist.class.getName()); | ||
private static final String DISABLED_PROPERTY = GroovySourceFileAllowlist.class.getName() + ".DISABLED"; | ||
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Non-final for script console access") | ||
static boolean DISABLED = SystemProperties.getBoolean(DISABLED_PROPERTY); | ||
|
||
/** | ||
* Checks whether a given Groovy source file is allowed to be loaded by {@link CpsFlowExecution#getTrustedShell}. | ||
* | ||
* @param groovySourceFileUrl the absolute URL to the Groovy source file as returned by {@link ClassLoader#getResource} | ||
* @return {@code true} if the Groovy source file may be loaded, {@code false} otherwise | ||
*/ | ||
public abstract boolean isAllowed(String groovySourceFileUrl); | ||
|
||
public static List<GroovySourceFileAllowlist> all() { | ||
return ExtensionList.lookup(GroovySourceFileAllowlist.class); | ||
} | ||
|
||
/** | ||
* {@link ClassLoader} that acts normally except for returning {@code null} from {@link #getResource} and | ||
* {@link #getResources} when looking up Groovy source files if the files are not allowed by | ||
* {@link GroovySourceFileAllowlist}. | ||
*/ | ||
static class ClassLoaderImpl extends ClassLoader { | ||
private static final String LOG_MESSAGE_TEMPLATE = | ||
"Preventing {0} from being loaded without sandbox protection in {1}. " + | ||
"To allow access to this file, add any suffix of its URL to the system property ‘" + | ||
DefaultAllowlist.ALLOWED_SOURCE_FILES_PROPERTY + "’ (use commas to separate multiple files). If you " + | ||
"want to allow any Groovy file on the Jenkins classpath to be accessed, you may set the system " + | ||
"property ‘" + DISABLED_PROPERTY + "’ to true."; | ||
|
||
private final String owner; | ||
|
||
public ClassLoaderImpl(@CheckForNull CpsFlowExecution execution, ClassLoader parent) { | ||
super(parent); | ||
this.owner = describeOwner(execution); | ||
} | ||
|
||
private static String describeOwner(@CheckForNull CpsFlowExecution execution) { | ||
if (execution != null) { | ||
try { | ||
return execution.getOwner().getExecutable().toString(); | ||
} catch (IOException e) { | ||
// Not significant in this context. | ||
} | ||
} | ||
return "unknown"; | ||
} | ||
|
||
@Override | ||
public URL getResource(String name) { | ||
URL url = super.getResource(name); | ||
if (DISABLED || url == null || !endsWithIgnoreCase(name, ".groovy") || isAllowed(url)) { | ||
return url; | ||
} | ||
// Note: This message gets printed twice because of https://github.com/apache/groovy/blob/41b990d0a20e442f29247f0e04cbed900f3dcad4/src/main/org/codehaus/groovy/control/ClassNodeResolver.java#L184-L186. | ||
LOGGER.log(Level.WARNING, LOG_MESSAGE_TEMPLATE, new Object[] { url, owner }); | ||
return null; | ||
} | ||
|
||
@Override | ||
public Enumeration<URL> getResources(String name) throws IOException { | ||
Enumeration<URL> urls = super.getResources(name); | ||
if (DISABLED || !urls.hasMoreElements() || !endsWithIgnoreCase(name, ".groovy")) { | ||
return urls; | ||
} | ||
List<URL> filteredUrls = new ArrayList<>(); | ||
while (urls.hasMoreElements()) { | ||
URL url = urls.nextElement(); | ||
if (isAllowed(url)) { | ||
filteredUrls.add(url); | ||
} else { | ||
LOGGER.log(Level.WARNING, LOG_MESSAGE_TEMPLATE, new Object[] { url, owner }); | ||
} | ||
} | ||
return Collections.enumeration(filteredUrls); | ||
} | ||
|
||
private static boolean isAllowed(URL url) { | ||
String urlString = url.toString(); | ||
for (GroovySourceFileAllowlist allowlist : GroovySourceFileAllowlist.all()) { | ||
if (allowlist.isAllowed(urlString)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
private static boolean endsWithIgnoreCase(String value, String suffix) { | ||
int suffixLength = suffix.length(); | ||
return value.regionMatches(true, value.length() - suffixLength, suffix, 0, suffixLength); | ||
} | ||
} | ||
|
||
/** | ||
* Allows Groovy source files used to implement DSLs in plugins that were created before | ||
* {@link GroovySourceFileAllowlist} was introduced. | ||
*/ | ||
@Extension | ||
public static class DefaultAllowlist extends GroovySourceFileAllowlist { | ||
private static final Logger LOGGER = Logger.getLogger(DefaultAllowlist.class.getName()); | ||
private static final String ALLOWED_SOURCE_FILES_PROPERTY = DefaultAllowlist.class.getCanonicalName() + ".ALLOWED_SOURCE_FILES"; | ||
/** | ||
* A list containing suffixes of known-good Groovy source file URLs that need to be accessible to Pipeline code. | ||
*/ | ||
/* Note: Actual ClassLoader resource URLs depend on environmental factors such as webroot settings and whether | ||
* we are currently testing one of the plugins in the list, so default-allowlist only contains the path | ||
* component of the resource URLs, and we allow any resource URL that ends with one of the entries in the list. | ||
* | ||
* We could try to load the exact URLs at runtime, but then we would have to account for dynamic plugin loading | ||
* (especially when a new Jenkins controller is initialized) and the fact that workflow-cps is always a | ||
* dependency of these plugins. | ||
*/ | ||
static final List<String> ALLOWED_SOURCE_FILES = new ArrayList<>(); | ||
|
||
public DefaultAllowlist() throws IOException { | ||
// We load custom entries first to improve performance in case .groovy is used for the property. | ||
String propertyValue = SystemProperties.getString(ALLOWED_SOURCE_FILES_PROPERTY, ""); | ||
for (String groovyFile : propertyValue.split(",")) { | ||
groovyFile = StringUtils.trimToNull(groovyFile); | ||
if (groovyFile != null) { | ||
if (groovyFile.endsWith(".groovy")) { | ||
ALLOWED_SOURCE_FILES.add(groovyFile); | ||
LOGGER.log(Level.INFO, "Allowing Pipelines to access {0}", groovyFile); | ||
} else { | ||
LOGGER.log(Level.WARNING, "Ignoring invalid Groovy source file: {0}", groovyFile); | ||
} | ||
} | ||
} | ||
loadDefaultAllowlist(ALLOWED_SOURCE_FILES); | ||
// Some plugins use test-specific Groovy DSLs. | ||
if (Main.isUnitTest) { | ||
ALLOWED_SOURCE_FILES.addAll(Arrays.asList( | ||
// pipeline-model-definition | ||
"/org/jenkinsci/plugins/pipeline/modeldefinition/agent/impl/LabelAndOtherFieldAgentScript.groovy", | ||
"/org/jenkinsci/plugins/pipeline/modeldefinition/parser/GlobalStageNameTestConditionalScript.groovy" | ||
)); | ||
} | ||
} | ||
|
||
private static void loadDefaultAllowlist(List<String> allowlist) throws IOException { | ||
try (InputStream is = GroovySourceFileAllowlist.class.getResourceAsStream("GroovySourceFileAllowlist/default-allowlist"); | ||
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));) { | ||
String line; | ||
while ((line = reader.readLine()) != null) { | ||
line = line.trim(); | ||
if (!line.isEmpty() && !line.startsWith("#")) { | ||
allowlist.add(line); | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public boolean isAllowed(String groovySourceFileUrl) { | ||
for (String sourceFile : ALLOWED_SOURCE_FILES) { | ||
if (groovySourceFileUrl.endsWith(sourceFile)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
} | ||
|
||
} |
53 changes: 53 additions & 0 deletions
53
.../resources/org/jenkinsci/plugins/workflow/cps/GroovySourceFileAllowlist/default-allowlist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# This list is ordered from most popular to least popular plugin to minimize performance impact. | ||
# pipeline-model-definition | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/ModelInterpreter.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/agent/impl/AnyScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/agent/impl/LabelScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/agent/impl/NoneScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/AbstractChangelogConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/AllOfConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/AnyOfConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/BranchConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/ChangeLogConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/ChangeRequestConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/ChangeSetConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/EnvironmentConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/EqualsConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/ExpressionConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/IsRestartedRunConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/NotConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/TagConditionalScript.groovy | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/when/impl/TriggeredByConditionalScript.groovy | ||
# pipeline-model-extensions | ||
/org/jenkinsci/plugins/pipeline/modeldefinition/agent/CheckoutScript.groovy | ||
# docker-workflow | ||
/org/jenkinsci/plugins/docker/workflow/Docker.groovy | ||
/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerPipelineScript.groovy | ||
/org/jenkinsci/plugins/docker/workflow/declarative/DockerPipelineFromDockerfileScript.groovy | ||
/org/jenkinsci/plugins/docker/workflow/declarative/DockerPipelineScript.groovy | ||
# kubernetes | ||
/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgentScript.groovy | ||
# amazon-ecs | ||
/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSDeclarativeAgentScript.groovy | ||
# workflow-remote-loader: | ||
/org/jenkinsci/plugins/workflow/remoteloader/FileLoaderDSL/FileLoaderDSLImpl.groovy | ||
# confluence-publisher | ||
/com/myyearbook/hudson/plugins/confluence/publishConfluence.groovy | ||
# openshift-client | ||
/com/openshift/jenkins/plugins/OpenShiftDSL.groovy | ||
# ownership: | ||
/org/jenkinsci/plugins/ownership/model/workflow/OwnershipGlobalVariable/Impl.groovy | ||
# templating-engine: | ||
/org/boozallen/plugins/jte/init/primitives/hooks/AnnotatedMethod.groovy | ||
/org/boozallen/plugins/jte/init/primitives/hooks/Hooks.groovy | ||
# datetime-constraint | ||
/org/jenkinsci/plugins/curfew/Checkpoint.groovy | ||
/org/jenkinsci/plugins/curfew/Curfew.groovy | ||
# redis-notifier | ||
/com/tsoft/jenkins/plugin/RedisClient.groovy | ||
# alauda-pipeline | ||
/io/alauda/jenkins/plugins/pipeline/AlaudaDSL.groovy | ||
# alauda-devops-pipeline | ||
/com/alauda/jenkins/plugins/AlaudaDevopsDSL.groovy | ||
/com/alauda/jenkins/plugins/AlaudaPlatformDSL.groovy | ||
/com/alauda/jenkins/plugins/StorageDSL.groovy |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters