blob: 5f2c2c47fe3592f37f7a9480458c3addf94a34e8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.codehaus.groovy.control.customizers;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.GroovyCodeVisitor;
import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ArrayExpression;
import org.codehaus.groovy.ast.expr.AttributeExpression;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.BitwiseNegationExpression;
import org.codehaus.groovy.ast.expr.BooleanExpression;
import org.codehaus.groovy.ast.expr.CastExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ClosureListExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.ElvisOperatorExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.FieldExpression;
import org.codehaus.groovy.ast.expr.GStringExpression;
import org.codehaus.groovy.ast.expr.LambdaExpression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.MapExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.MethodPointerExpression;
import org.codehaus.groovy.ast.expr.MethodReferenceExpression;
import org.codehaus.groovy.ast.expr.NotExpression;
import org.codehaus.groovy.ast.expr.PostfixExpression;
import org.codehaus.groovy.ast.expr.PrefixExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.RangeExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.SpreadMapExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.UnaryMinusExpression;
import org.codehaus.groovy.ast.expr.UnaryPlusExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.AssertStatement;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.BreakStatement;
import org.codehaus.groovy.ast.stmt.CaseStatement;
import org.codehaus.groovy.ast.stmt.CatchStatement;
import org.codehaus.groovy.ast.stmt.ContinueStatement;
import org.codehaus.groovy.ast.stmt.DoWhileStatement;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.ForStatement;
import org.codehaus.groovy.ast.stmt.IfStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.ast.stmt.SwitchStatement;
import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
import org.codehaus.groovy.classgen.BytecodeExpression;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.syntax.Token;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* This customizer allows securing source code by controlling what code constructs are allowed. For example, if you only
* want to allow arithmetic operations in a groovy shell, you can configure this customizer to restrict package imports,
* method calls and so on.
* <p>
* Most of the security customization options found in this class work with either blacklist or whitelist. This means that, for a
* single option, you can set a whitelist OR a blacklist, but not both. You can mix whitelist/blacklist strategies for
* different options. For example, you can have import whitelist and tokens blacklist.
* <p>
* The recommended way of securing shells is to use whitelists because it is guaranteed that future features of the
* Groovy language won't be allowed by defaut. Using blacklists, you can limit the features of the languages by opting
* out, but new language features would require you to update your configuration.
* <p>
* If you set neither a whitelist nor a blacklist, then everything is authorized.
* <p>
* Combinations of import and star imports constraints are authorized as long as you use the same type of list for both.
* For example, you may use an import whitelist and a star import whitelist together, but you cannot use an import white
* list with a star import blacklist. static imports are handled separately, meaning that blacklisting an import <b>
* does not</b> prevent from using a static import.
* <p>
* Eventually, if the features provided here are not sufficient, you may implement custom AST filtering handlers, either
* implementing the {@link StatementChecker} interface or {@link ExpressionChecker} interface then register your
* handlers thanks to the {@link #addExpressionCheckers(org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker...)}
* and {@link #addStatementCheckers(org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker...)}
* methods.
* <p>
* Here is an example of usage. We will create a groovy classloader which only supports arithmetic operations and imports
* the java.lang.Math classes by default.
*
* <pre>
* final ImportCustomizer imports = new ImportCustomizer().addStaticStars('java.lang.Math') // add static import of java.lang.Math
* final SecureASTCustomizer secure = new SecureASTCustomizer()
* secure.with {
* closuresAllowed = false
* methodDefinitionAllowed = false
*
* importsWhitelist = []
* staticImportsWhitelist = []
* staticStarImportsWhitelist = ['java.lang.Math'] // only java.lang.Math is allowed
*
* tokensWhitelist = [
* PLUS,
* MINUS,
* MULTIPLY,
* DIVIDE,
* MOD,
* POWER,
* PLUS_PLUS,
* MINUS_MINUS,
* COMPARE_EQUAL,
* COMPARE_NOT_EQUAL,
* COMPARE_LESS_THAN,
* COMPARE_LESS_THAN_EQUAL,
* COMPARE_GREATER_THAN,
* COMPARE_GREATER_THAN_EQUAL,
* ].asImmutable()
*
* constantTypesClassesWhiteList = [
* Integer,
* Float,
* Long,
* Double,
* BigDecimal,
* Integer.TYPE,
* Long.TYPE,
* Float.TYPE,
* Double.TYPE
* ].asImmutable()
*
* receiversClassesWhiteList = [
* Math,
* Integer,
* Float,
* Double,
* Long,
* BigDecimal
* ].asImmutable()
* }
* CompilerConfiguration config = new CompilerConfiguration()
* config.addCompilationCustomizers(imports, secure)
* GroovyClassLoader loader = new GroovyClassLoader(this.class.classLoader, config)
* </pre>
*
* @since 1.8.0
*/
public class SecureASTCustomizer extends CompilationCustomizer {
private boolean isPackageAllowed = true;
private boolean isMethodDefinitionAllowed = true;
private boolean isClosuresAllowed = true;
// imports
private List<String> importsWhitelist;
private List<String> importsBlacklist;
// static imports
private List<String> staticImportsWhitelist;
private List<String> staticImportsBlacklist;
// star imports
private List<String> starImportsWhitelist;
private List<String> starImportsBlacklist;
// static star imports
private List<String> staticStarImportsWhitelist;
private List<String> staticStarImportsBlacklist;
// indirect import checks
// if set to true, then security rules on imports will also be applied on classnodes.
// Direct instantiation of classes without imports will therefore also fail if this option is enabled
private boolean isIndirectImportCheckEnabled;
// statements
private List<Class<? extends Statement>> statementsWhitelist;
private List<Class<? extends Statement>> statementsBlacklist;
private final List<StatementChecker> statementCheckers = new LinkedList<StatementChecker>();
// expressions
private List<Class<? extends Expression>> expressionsWhitelist;
private List<Class<? extends Expression>> expressionsBlacklist;
private final List<ExpressionChecker> expressionCheckers = new LinkedList<ExpressionChecker>();
// tokens from Types
private List<Integer> tokensWhitelist;
private List<Integer> tokensBlacklist;
// constant types
private List<String> constantTypesWhiteList;
private List<String> constantTypesBlackList;
// receivers
private List<String> receiversWhiteList;
private List<String> receiversBlackList;
public SecureASTCustomizer() {
super(CompilePhase.CANONICALIZATION);
}
public boolean isMethodDefinitionAllowed() {
return isMethodDefinitionAllowed;
}
public void setMethodDefinitionAllowed(final boolean methodDefinitionAllowed) {
isMethodDefinitionAllowed = methodDefinitionAllowed;
}
public boolean isPackageAllowed() {
return isPackageAllowed;
}
public boolean isClosuresAllowed() {
return isClosuresAllowed;
}
public void setClosuresAllowed(final boolean closuresAllowed) {
isClosuresAllowed = closuresAllowed;
}
public void setPackageAllowed(final boolean packageAllowed) {
isPackageAllowed = packageAllowed;
}
public List<String> getImportsBlacklist() {
return importsBlacklist;
}
public void setImportsBlacklist(final List<String> importsBlacklist) {
if (importsWhitelist != null || starImportsWhitelist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.importsBlacklist = importsBlacklist;
}
public List<String> getImportsWhitelist() {
return importsWhitelist;
}
public void setImportsWhitelist(final List<String> importsWhitelist) {
if (importsBlacklist != null || starImportsBlacklist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.importsWhitelist = importsWhitelist;
}
public List<String> getStarImportsBlacklist() {
return starImportsBlacklist;
}
public void setStarImportsBlacklist(final List<String> starImportsBlacklist) {
if (importsWhitelist != null || starImportsWhitelist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.starImportsBlacklist = normalizeStarImports(starImportsBlacklist);
if (this.importsBlacklist == null) importsBlacklist = Collections.emptyList();
}
public List<String> getStarImportsWhitelist() {
return starImportsWhitelist;
}
public void setStarImportsWhitelist(final List<String> starImportsWhitelist) {
if (importsBlacklist != null || starImportsBlacklist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.starImportsWhitelist = normalizeStarImports(starImportsWhitelist);
if (this.importsWhitelist == null) importsWhitelist = Collections.emptyList();
}
/**
* Ensures that every star import ends with .* as this is the expected syntax in import checks.
*/
private static List<String> normalizeStarImports(List<String> starImports) {
List<String> result = new ArrayList<String>(starImports.size());
for (String starImport : starImports) {
if (starImport.endsWith(".*")) {
result.add(starImport);
} else if (starImport.endsWith(".")) {
result.add(starImport + "*");
} else {
result.add(starImport + ".*");
}
}
return Collections.unmodifiableList(result);
}
public List<String> getStaticImportsBlacklist() {
return staticImportsBlacklist;
}
public void setStaticImportsBlacklist(final List<String> staticImportsBlacklist) {
if (staticImportsWhitelist != null || staticStarImportsWhitelist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.staticImportsBlacklist = staticImportsBlacklist;
}
public List<String> getStaticImportsWhitelist() {
return staticImportsWhitelist;
}
public void setStaticImportsWhitelist(final List<String> staticImportsWhitelist) {
if (staticImportsBlacklist != null || staticStarImportsBlacklist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.staticImportsWhitelist = staticImportsWhitelist;
}
public List<String> getStaticStarImportsBlacklist() {
return staticStarImportsBlacklist;
}
public void setStaticStarImportsBlacklist(final List<String> staticStarImportsBlacklist) {
if (staticImportsWhitelist != null || staticStarImportsWhitelist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.staticStarImportsBlacklist = normalizeStarImports(staticStarImportsBlacklist);
if (this.staticImportsBlacklist == null) this.staticImportsBlacklist = Collections.emptyList();
}
public List<String> getStaticStarImportsWhitelist() {
return staticStarImportsWhitelist;
}
public void setStaticStarImportsWhitelist(final List<String> staticStarImportsWhitelist) {
if (staticImportsBlacklist != null || staticStarImportsBlacklist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.staticStarImportsWhitelist = normalizeStarImports(staticStarImportsWhitelist);
if (this.staticImportsWhitelist == null) this.staticImportsWhitelist = Collections.emptyList();
}
public List<Class<? extends Expression>> getExpressionsBlacklist() {
return expressionsBlacklist;
}
public void setExpressionsBlacklist(final List<Class<? extends Expression>> expressionsBlacklist) {
if (expressionsWhitelist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.expressionsBlacklist = expressionsBlacklist;
}
public List<Class<? extends Expression>> getExpressionsWhitelist() {
return expressionsWhitelist;
}
public void setExpressionsWhitelist(final List<Class<? extends Expression>> expressionsWhitelist) {
if (expressionsBlacklist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.expressionsWhitelist = expressionsWhitelist;
}
public List<Class<? extends Statement>> getStatementsBlacklist() {
return statementsBlacklist;
}
public void setStatementsBlacklist(final List<Class<? extends Statement>> statementsBlacklist) {
if (statementsWhitelist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.statementsBlacklist = statementsBlacklist;
}
public List<Class<? extends Statement>> getStatementsWhitelist() {
return statementsWhitelist;
}
public void setStatementsWhitelist(final List<Class<? extends Statement>> statementsWhitelist) {
if (statementsBlacklist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.statementsWhitelist = statementsWhitelist;
}
public List<Integer> getTokensBlacklist() {
return tokensBlacklist;
}
public boolean isIndirectImportCheckEnabled() {
return isIndirectImportCheckEnabled;
}
/**
* Set this option to true if you want your import rules to be checked against every class node. This means that if
* someone uses a fully qualified class name, then it will also be checked against the import rules, preventing, for
* example, instantiation of classes without imports thanks to FQCN.
*
* @param indirectImportCheckEnabled set to true to enable indirect checks
*/
public void setIndirectImportCheckEnabled(final boolean indirectImportCheckEnabled) {
isIndirectImportCheckEnabled = indirectImportCheckEnabled;
}
/**
* Sets the list of tokens which are blacklisted.
*
* @param tokensBlacklist the tokens. The values of the tokens must be those of {@link org.codehaus.groovy.syntax.Types}
*/
public void setTokensBlacklist(final List<Integer> tokensBlacklist) {
if (tokensWhitelist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.tokensBlacklist = tokensBlacklist;
}
public List<Integer> getTokensWhitelist() {
return tokensWhitelist;
}
/**
* Sets the list of tokens which are whitelisted.
*
* @param tokensWhitelist the tokens. The values of the tokens must be those of {@link org.codehaus.groovy.syntax.Types}
*/
public void setTokensWhitelist(final List<Integer> tokensWhitelist) {
if (tokensBlacklist != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.tokensWhitelist = tokensWhitelist;
}
public void addStatementCheckers(StatementChecker... checkers) {
statementCheckers.addAll(Arrays.asList(checkers));
}
public void addExpressionCheckers(ExpressionChecker... checkers) {
expressionCheckers.addAll(Arrays.asList(checkers));
}
public List<String> getConstantTypesBlackList() {
return constantTypesBlackList;
}
public void setConstantTypesBlackList(final List<String> constantTypesBlackList) {
if (constantTypesWhiteList != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.constantTypesBlackList = constantTypesBlackList;
}
public List<String> getConstantTypesWhiteList() {
return constantTypesWhiteList;
}
public void setConstantTypesWhiteList(final List<String> constantTypesWhiteList) {
if (constantTypesBlackList != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.constantTypesWhiteList = constantTypesWhiteList;
}
/**
* An alternative way of setting constant types.
*
* @param constantTypesWhiteList a list of classes.
*/
public void setConstantTypesClassesWhiteList(final List<Class> constantTypesWhiteList) {
List<String> values = new LinkedList<String>();
for (Class aClass : constantTypesWhiteList) {
values.add(aClass.getName());
}
setConstantTypesWhiteList(values);
}
/**
* An alternative way of setting constant types.
*
* @param constantTypesBlackList a list of classes.
*/
public void setConstantTypesClassesBlackList(final List<Class> constantTypesBlackList) {
List<String> values = new LinkedList<String>();
for (Class aClass : constantTypesBlackList) {
values.add(aClass.getName());
}
setConstantTypesBlackList(values);
}
public List<String> getReceiversBlackList() {
return receiversBlackList;
}
/**
* Sets the list of classes which deny method calls.
*
* Please note that since Groovy is a dynamic language, and
* this class performs a static type check, it will be reletively
* simple to bypass any blacklist unless the receivers blacklist contains, at
* a minimum, Object, Script, GroovyShell, and Eval. Additionally,
* it is necessary to also blacklist MethodPointerExpression in the
* expressions blacklist for the receivers blacklist to function
* as a security check.
*
* @param receiversBlackList the list of refused classes, as fully qualified names
*/
public void setReceiversBlackList(final List<String> receiversBlackList) {
if (receiversWhiteList != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.receiversBlackList = receiversBlackList;
}
/**
* An alternative way of setting {@link #setReceiversBlackList(java.util.List) receiver classes}.
*
* @param receiversBlacklist a list of classes.
*/
public void setReceiversClassesBlackList(final List<Class> receiversBlacklist) {
List<String> values = new LinkedList<String>();
for (Class aClass : receiversBlacklist) {
values.add(aClass.getName());
}
setReceiversBlackList(values);
}
public List<String> getReceiversWhiteList() {
return receiversWhiteList;
}
/**
* Sets the list of classes which may accept method calls.
*
* @param receiversWhiteList the list of accepted classes, as fully qualified names
*/
public void setReceiversWhiteList(final List<String> receiversWhiteList) {
if (receiversBlackList != null) {
throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist");
}
this.receiversWhiteList = receiversWhiteList;
}
/**
* An alternative way of setting {@link #setReceiversWhiteList(java.util.List) receiver classes}.
*
* @param receiversWhitelist a list of classes.
*/
public void setReceiversClassesWhiteList(final List<Class> receiversWhitelist) {
List<String> values = new LinkedList<String>();
for (Class aClass : receiversWhitelist) {
values.add(aClass.getName());
}
setReceiversWhiteList(values);
}
@Override
public void call(final SourceUnit source, final GeneratorContext context, final ClassNode classNode) throws CompilationFailedException {
final ModuleNode ast = source.getAST();
if (!isPackageAllowed && ast.getPackage() != null) {
throw new SecurityException("Package definitions are not allowed");
}
checkMethodDefinitionAllowed(classNode);
// verify imports
if (importsBlacklist != null || importsWhitelist != null || starImportsBlacklist != null || starImportsWhitelist != null) {
for (ImportNode importNode : ast.getImports()) {
final String className = importNode.getClassName();
assertImportIsAllowed(className);
}
for (ImportNode importNode : ast.getStarImports()) {
final String className = importNode.getPackageName();
assertStarImportIsAllowed(className + "*");
}
}
// verify static imports
if (staticImportsBlacklist != null || staticImportsWhitelist != null || staticStarImportsBlacklist != null || staticStarImportsWhitelist != null) {
for (Map.Entry<String, ImportNode> entry : ast.getStaticImports().entrySet()) {
final String className = entry.getValue().getClassName();
assertStaticImportIsAllowed(entry.getKey(), className);
}
for (Map.Entry<String, ImportNode> entry : ast.getStaticStarImports().entrySet()) {
final String className = entry.getValue().getClassName();
assertStaticImportIsAllowed(entry.getKey(), className);
}
}
final GroovyCodeVisitor visitor = createGroovyCodeVisitor();
ast.getStatementBlock().visit(visitor);
for (ClassNode clNode : ast.getClasses()) {
if (clNode!=classNode) {
checkMethodDefinitionAllowed(clNode);
for (MethodNode methodNode : clNode.getMethods()) {
if (!methodNode.isSynthetic() && methodNode.getCode() != null) {
methodNode.getCode().visit(visitor);
}
}
}
}
List<MethodNode> methods = filterMethods(classNode);
if (isMethodDefinitionAllowed) {
for (MethodNode method : methods) {
if (method.getDeclaringClass()==classNode && method.getCode() != null) method.getCode().visit(visitor);
}
}
}
protected GroovyCodeVisitor createGroovyCodeVisitor() {
return new SecuringCodeVisitor();
}
protected void checkMethodDefinitionAllowed(ClassNode owner) {
if (isMethodDefinitionAllowed) return;
List<MethodNode> methods = filterMethods(owner);
if (!methods.isEmpty()) throw new SecurityException("Method definitions are not allowed");
}
protected static List<MethodNode> filterMethods(ClassNode owner) {
List<MethodNode> result = new LinkedList<MethodNode>();
List<MethodNode> methods = owner.getMethods();
for (MethodNode method : methods) {
if (method.getDeclaringClass() == owner && !method.isSynthetic()) {
if ("main".equals(method.getName()) || "run".equals(method.getName()) && owner.isScriptBody()) continue;
result.add(method);
}
}
return result;
}
protected void assertStarImportIsAllowed(final String packageName) {
if (starImportsWhitelist != null && !starImportsWhitelist.contains(packageName)) {
throw new SecurityException("Importing [" + packageName + "] is not allowed");
}
if (starImportsBlacklist != null && starImportsBlacklist.contains(packageName)) {
throw new SecurityException("Importing [" + packageName + "] is not allowed");
}
}
protected void assertImportIsAllowed(final String className) {
if (importsWhitelist != null && !importsWhitelist.contains(className)) {
if (starImportsWhitelist != null) {
// we should now check if the import is in the star imports
ClassNode node = ClassHelper.make(className);
final String packageName = node.getPackageName();
if (!starImportsWhitelist.contains(packageName + ".*")) {
throw new SecurityException("Importing [" + className + "] is not allowed");
}
} else {
throw new SecurityException("Importing [" + className + "] is not allowed");
}
}
if (importsBlacklist != null && importsBlacklist.contains(className)) {
throw new SecurityException("Importing [" + className + "] is not allowed");
}
// check that there's no star import blacklist
if (starImportsBlacklist != null) {
ClassNode node = ClassHelper.make(className);
final String packageName = node.getPackageName();
if (starImportsBlacklist.contains(packageName + ".*")) {
throw new SecurityException("Importing [" + className + "] is not allowed");
}
}
}
protected void assertStaticImportIsAllowed(final String member, final String className) {
final String fqn = member.equals(className) ? member : className + "." + member;
if (staticImportsWhitelist != null && !staticImportsWhitelist.contains(fqn)) {
if (staticStarImportsWhitelist != null) {
// we should now check if the import is in the star imports
if (!staticStarImportsWhitelist.contains(className + ".*")) {
throw new SecurityException("Importing [" + fqn + "] is not allowed");
}
} else {
throw new SecurityException("Importing [" + fqn + "] is not allowed");
}
}
if (staticImportsBlacklist != null && staticImportsBlacklist.contains(fqn)) {
throw new SecurityException("Importing [" + fqn + "] is not allowed");
}
// check that there's no star import blacklist
if (staticStarImportsBlacklist != null) {
if (staticStarImportsBlacklist.contains(className + ".*")) {
throw new SecurityException("Importing [" + fqn + "] is not allowed");
}
}
}
/**
* This visitor directly implements the {@link GroovyCodeVisitor} interface instead of using the {@link
* CodeVisitorSupport} class to make sure that future features of the language gets managed by this visitor. Thus,
* adding a new feature would result in a compilation error if this visitor is not updated.
*/
protected class SecuringCodeVisitor implements GroovyCodeVisitor {
/**
* Checks that a given statement is either in the whitelist or not in the blacklist.
*
* @param statement the statement to be checked
* @throws SecurityException if usage of this statement class is forbidden
*/
protected void assertStatementAuthorized(final Statement statement) throws SecurityException {
final Class<? extends Statement> clazz = statement.getClass();
if (statementsBlacklist != null && statementsBlacklist.contains(clazz)) {
throw new SecurityException(clazz.getSimpleName() + "s are not allowed");
} else if (statementsWhitelist != null && !statementsWhitelist.contains(clazz)) {
throw new SecurityException(clazz.getSimpleName() + "s are not allowed");
}
for (StatementChecker statementChecker : statementCheckers) {
if (!statementChecker.isAuthorized(statement)) {
throw new SecurityException("Statement [" + clazz.getSimpleName() + "] is not allowed");
}
}
}
/**
* Checks that a given expression is either in the whitelist or not in the blacklist.
*
* @param expression the expression to be checked
* @throws SecurityException if usage of this expression class is forbidden
*/
protected void assertExpressionAuthorized(final Expression expression) throws SecurityException {
final Class<? extends Expression> clazz = expression.getClass();
if (expressionsBlacklist != null && expressionsBlacklist.contains(clazz)) {
throw new SecurityException(clazz.getSimpleName() + "s are not allowed: " + expression.getText());
} else if (expressionsWhitelist != null && !expressionsWhitelist.contains(clazz)) {
throw new SecurityException(clazz.getSimpleName() + "s are not allowed: " + expression.getText());
}
for (ExpressionChecker expressionChecker : expressionCheckers) {
if (!expressionChecker.isAuthorized(expression)) {
throw new SecurityException("Expression [" + clazz.getSimpleName() + "] is not allowed: " + expression.getText());
}
}
if (isIndirectImportCheckEnabled) {
try {
if (expression instanceof ConstructorCallExpression) {
assertImportIsAllowed(expression.getType().getName());
} else if (expression instanceof MethodCallExpression) {
MethodCallExpression expr = (MethodCallExpression) expression;
ClassNode objectExpressionType = expr.getObjectExpression().getType();
final String typename = getExpressionType(objectExpressionType).getName();
assertImportIsAllowed(typename);
assertStaticImportIsAllowed(expr.getMethodAsString(), typename);
} else if (expression instanceof StaticMethodCallExpression) {
StaticMethodCallExpression expr = (StaticMethodCallExpression) expression;
final String typename = expr.getOwnerType().getName();
assertImportIsAllowed(typename);
assertStaticImportIsAllowed(expr.getMethod(), typename);
} else if (expression instanceof MethodPointerExpression) {
MethodPointerExpression expr = (MethodPointerExpression) expression;
final String typename = expr.getType().getName();
assertImportIsAllowed(typename);
assertStaticImportIsAllowed(expr.getText(), typename);
}
} catch (SecurityException e) {
throw new SecurityException("Indirect import checks prevents usage of expression", e);
}
}
}
protected ClassNode getExpressionType(ClassNode objectExpressionType) {
return objectExpressionType.isArray() ? getExpressionType(objectExpressionType.getComponentType()) : objectExpressionType;
}
/**
* Checks that a given token is either in the whitelist or not in the blacklist.
*
* @param token the token to be checked
* @throws SecurityException if usage of this token is forbidden
*/
protected void assertTokenAuthorized(final Token token) throws SecurityException {
final int value = token.getType();
if (tokensBlacklist != null && tokensBlacklist.contains(value)) {
throw new SecurityException("Token " + token + " is not allowed");
} else if (tokensWhitelist != null && !tokensWhitelist.contains(value)) {
throw new SecurityException("Token " + token + " is not allowed");
}
}
@Override
public void visitBlockStatement(final BlockStatement block) {
assertStatementAuthorized(block);
for (Statement statement : block.getStatements()) {
statement.visit(this);
}
}
@Override
public void visitForLoop(final ForStatement forLoop) {
assertStatementAuthorized(forLoop);
forLoop.getCollectionExpression().visit(this);
forLoop.getLoopBlock().visit(this);
}
@Override
public void visitWhileLoop(final WhileStatement loop) {
assertStatementAuthorized(loop);
loop.getBooleanExpression().visit(this);
loop.getLoopBlock().visit(this);
}
@Override
public void visitDoWhileLoop(final DoWhileStatement loop) {
assertStatementAuthorized(loop);
loop.getBooleanExpression().visit(this);
loop.getLoopBlock().visit(this);
}
@Override
public void visitIfElse(final IfStatement ifElse) {
assertStatementAuthorized(ifElse);
ifElse.getBooleanExpression().visit(this);
ifElse.getIfBlock().visit(this);
ifElse.getElseBlock().visit(this);
}
@Override
public void visitExpressionStatement(final ExpressionStatement statement) {
assertStatementAuthorized(statement);
statement.getExpression().visit(this);
}
@Override
public void visitReturnStatement(final ReturnStatement statement) {
assertStatementAuthorized(statement);
statement.getExpression().visit(this);
}
@Override
public void visitAssertStatement(final AssertStatement statement) {
assertStatementAuthorized(statement);
statement.getBooleanExpression().visit(this);
statement.getMessageExpression().visit(this);
}
@Override
public void visitTryCatchFinally(final TryCatchStatement statement) {
assertStatementAuthorized(statement);
statement.getTryStatement().visit(this);
for (CatchStatement catchStatement : statement.getCatchStatements()) {
catchStatement.visit(this);
}
statement.getFinallyStatement().visit(this);
}
@Override
public void visitEmptyStatement(EmptyStatement statement) {
// noop
}
@Override
public void visitSwitch(final SwitchStatement statement) {
assertStatementAuthorized(statement);
statement.getExpression().visit(this);
for (CaseStatement caseStatement : statement.getCaseStatements()) {
caseStatement.visit(this);
}
statement.getDefaultStatement().visit(this);
}
@Override
public void visitCaseStatement(final CaseStatement statement) {
assertStatementAuthorized(statement);
statement.getExpression().visit(this);
statement.getCode().visit(this);
}
@Override
public void visitBreakStatement(final BreakStatement statement) {
assertStatementAuthorized(statement);
}
@Override
public void visitContinueStatement(final ContinueStatement statement) {
assertStatementAuthorized(statement);
}
@Override
public void visitThrowStatement(final ThrowStatement statement) {
assertStatementAuthorized(statement);
statement.getExpression().visit(this);
}
@Override
public void visitSynchronizedStatement(final SynchronizedStatement statement) {
assertStatementAuthorized(statement);
statement.getExpression().visit(this);
statement.getCode().visit(this);
}
@Override
public void visitCatchStatement(final CatchStatement statement) {
assertStatementAuthorized(statement);
statement.getCode().visit(this);
}
@Override
public void visitMethodCallExpression(final MethodCallExpression call) {
assertExpressionAuthorized(call);
Expression receiver = call.getObjectExpression();
final String typeName = receiver.getType().getName();
if (receiversWhiteList != null && !receiversWhiteList.contains(typeName)) {
throw new SecurityException("Method calls not allowed on [" + typeName + "]");
} else if (receiversBlackList != null && receiversBlackList.contains(typeName)) {
throw new SecurityException("Method calls not allowed on [" + typeName + "]");
}
receiver.visit(this);
final Expression method = call.getMethod();
checkConstantTypeIfNotMethodNameOrProperty(method);
call.getArguments().visit(this);
}
@Override
public void visitStaticMethodCallExpression(final StaticMethodCallExpression call) {
assertExpressionAuthorized(call);
final String typeName = call.getOwnerType().getName();
if (receiversWhiteList != null && !receiversWhiteList.contains(typeName)) {
throw new SecurityException("Method calls not allowed on [" + typeName + "]");
} else if (receiversBlackList != null && receiversBlackList.contains(typeName)) {
throw new SecurityException("Method calls not allowed on [" + typeName + "]");
}
call.getArguments().visit(this);
}
@Override
public void visitConstructorCallExpression(final ConstructorCallExpression call) {
assertExpressionAuthorized(call);
call.getArguments().visit(this);
}
@Override
public void visitTernaryExpression(final TernaryExpression expression) {
assertExpressionAuthorized(expression);
expression.getBooleanExpression().visit(this);
expression.getTrueExpression().visit(this);
expression.getFalseExpression().visit(this);
}
@Override
public void visitShortTernaryExpression(final ElvisOperatorExpression expression) {
assertExpressionAuthorized(expression);
visitTernaryExpression(expression);
}
@Override
public void visitBinaryExpression(final BinaryExpression expression) {
assertExpressionAuthorized(expression);
assertTokenAuthorized(expression.getOperation());
expression.getLeftExpression().visit(this);
expression.getRightExpression().visit(this);
}
@Override
public void visitPrefixExpression(final PrefixExpression expression) {
assertExpressionAuthorized(expression);
assertTokenAuthorized(expression.getOperation());
expression.getExpression().visit(this);
}
@Override
public void visitPostfixExpression(final PostfixExpression expression) {
assertExpressionAuthorized(expression);
assertTokenAuthorized(expression.getOperation());
expression.getExpression().visit(this);
}
@Override
public void visitBooleanExpression(final BooleanExpression expression) {
assertExpressionAuthorized(expression);
expression.getExpression().visit(this);
}
@Override
public void visitClosureExpression(final ClosureExpression expression) {
assertExpressionAuthorized(expression);
if (!isClosuresAllowed) throw new SecurityException("Closures are not allowed");
expression.getCode().visit(this);
}
@Override
public void visitLambdaExpression(LambdaExpression expression) {
visitClosureExpression(expression);
}
@Override
public void visitTupleExpression(final TupleExpression expression) {
assertExpressionAuthorized(expression);
visitListOfExpressions(expression.getExpressions());
}
@Override
public void visitMapExpression(final MapExpression expression) {
assertExpressionAuthorized(expression);
visitListOfExpressions(expression.getMapEntryExpressions());
}
@Override
public void visitMapEntryExpression(final MapEntryExpression expression) {
assertExpressionAuthorized(expression);
expression.getKeyExpression().visit(this);
expression.getValueExpression().visit(this);
}
@Override
public void visitListExpression(final ListExpression expression) {
assertExpressionAuthorized(expression);
visitListOfExpressions(expression.getExpressions());
}
@Override
public void visitRangeExpression(final RangeExpression expression) {
assertExpressionAuthorized(expression);
expression.getFrom().visit(this);
expression.getTo().visit(this);
}
@Override
public void visitPropertyExpression(final PropertyExpression expression) {
assertExpressionAuthorized(expression);
Expression receiver = expression.getObjectExpression();
final String typeName = receiver.getType().getName();
if (receiversWhiteList != null && !receiversWhiteList.contains(typeName)) {
throw new SecurityException("Property access not allowed on [" + typeName + "]");
} else if (receiversBlackList != null && receiversBlackList.contains(typeName)) {
throw new SecurityException("Property access not allowed on [" + typeName + "]");
}
receiver.visit(this);
final Expression property = expression.getProperty();
checkConstantTypeIfNotMethodNameOrProperty(property);
}
private void checkConstantTypeIfNotMethodNameOrProperty(final Expression expr) {
if (expr instanceof ConstantExpression) {
if (!"java.lang.String".equals(expr.getType().getName())) {
expr.visit(this);
}
} else {
expr.visit(this);
}
}
@Override
public void visitAttributeExpression(final AttributeExpression expression) {
assertExpressionAuthorized(expression);
Expression receiver = expression.getObjectExpression();
final String typeName = receiver.getType().getName();
if (receiversWhiteList != null && !receiversWhiteList.contains(typeName)) {
throw new SecurityException("Attribute access not allowed on [" + typeName + "]");
} else if (receiversBlackList != null && receiversBlackList.contains(typeName)) {
throw new SecurityException("Attribute access not allowed on [" + typeName + "]");
}
receiver.visit(this);
final Expression property = expression.getProperty();
checkConstantTypeIfNotMethodNameOrProperty(property);
}
@Override
public void visitFieldExpression(final FieldExpression expression) {
assertExpressionAuthorized(expression);
}
@Override
public void visitMethodPointerExpression(final MethodPointerExpression expression) {
assertExpressionAuthorized(expression);
expression.getExpression().visit(this);
expression.getMethodName().visit(this);
}
@Override
public void visitMethodReferenceExpression(final MethodReferenceExpression expression) {
visitMethodPointerExpression(expression);
}
@Override
public void visitConstantExpression(final ConstantExpression expression) {
assertExpressionAuthorized(expression);
final String type = expression.getType().getName();
if (constantTypesWhiteList != null && !constantTypesWhiteList.contains(type)) {
throw new SecurityException("Constant expression type [" + type + "] is not allowed");
}
if (constantTypesBlackList != null && constantTypesBlackList.contains(type)) {
throw new SecurityException("Constant expression type [" + type + "] is not allowed");
}
}
@Override
public void visitClassExpression(final ClassExpression expression) {
assertExpressionAuthorized(expression);
}
@Override
public void visitVariableExpression(final VariableExpression expression) {
assertExpressionAuthorized(expression);
final String type = expression.getType().getName();
if (constantTypesWhiteList != null && !constantTypesWhiteList.contains(type)) {
throw new SecurityException("Usage of variables of type [" + type + "] is not allowed");
}
if (constantTypesBlackList != null && constantTypesBlackList.contains(type)) {
throw new SecurityException("Usage of variables of type [" + type + "] is not allowed");
}
}
@Override
public void visitDeclarationExpression(final DeclarationExpression expression) {
assertExpressionAuthorized(expression);
visitBinaryExpression(expression);
}
@Override
public void visitGStringExpression(final GStringExpression expression) {
assertExpressionAuthorized(expression);
visitListOfExpressions(expression.getStrings());
visitListOfExpressions(expression.getValues());
}
@Override
public void visitArrayExpression(final ArrayExpression expression) {
assertExpressionAuthorized(expression);
visitListOfExpressions(expression.getExpressions());
visitListOfExpressions(expression.getSizeExpression());
}
@Override
public void visitSpreadExpression(final SpreadExpression expression) {
assertExpressionAuthorized(expression);
expression.getExpression().visit(this);
}
@Override
public void visitSpreadMapExpression(final SpreadMapExpression expression) {
assertExpressionAuthorized(expression);
expression.getExpression().visit(this);
}
@Override
public void visitNotExpression(final NotExpression expression) {
assertExpressionAuthorized(expression);
expression.getExpression().visit(this);
}
@Override
public void visitUnaryMinusExpression(final UnaryMinusExpression expression) {
assertExpressionAuthorized(expression);
expression.getExpression().visit(this);
}
@Override
public void visitUnaryPlusExpression(final UnaryPlusExpression expression) {
assertExpressionAuthorized(expression);
expression.getExpression().visit(this);
}
@Override
public void visitBitwiseNegationExpression(final BitwiseNegationExpression expression) {
assertExpressionAuthorized(expression);
expression.getExpression().visit(this);
}
@Override
public void visitCastExpression(final CastExpression expression) {
assertExpressionAuthorized(expression);
expression.getExpression().visit(this);
}
@Override
public void visitArgumentlistExpression(final ArgumentListExpression expression) {
assertExpressionAuthorized(expression);
visitTupleExpression(expression);
}
@Override
public void visitClosureListExpression(final ClosureListExpression closureListExpression) {
assertExpressionAuthorized(closureListExpression);
if (!isClosuresAllowed) throw new SecurityException("Closures are not allowed");
visitListOfExpressions(closureListExpression.getExpressions());
}
@Override
public void visitBytecodeExpression(final BytecodeExpression expression) {
assertExpressionAuthorized(expression);
}
}
/**
* This interface allows the user to plugin custom expression checkers if expression blacklist or whitelist are not
* sufficient
*/
@FunctionalInterface
public interface ExpressionChecker {
boolean isAuthorized(Expression expression);
}
/**
* This interface allows the user to plugin custom statement checkers if statement blacklist or whitelist are not
* sufficient
*/
@FunctionalInterface
public interface StatementChecker {
boolean isAuthorized(Statement expression);
}
}