Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,566 changes: 1,566 additions & 0 deletions org/codehaus/groovy/control/customizers/SecureASTCustomizer.java

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions xxl-job-admin/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions xxl-job-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@
<scope>provided</scope>
</dependency>

<!-- test -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> allowedGlueTypes = new HashSet<>(Arrays.asList("BEAN", "GLUE_GROOVY"));

public void setAdminAddresses(String adminAddresses) {
this.adminAddresses = adminAddresses;
}
Expand Down Expand Up @@ -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<String> 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<String> getAllowedGlueTypes() {
return allowedGlueTypes;
}


// ---------------------- start + stop ----------------------
public void start() throws Exception {
Expand Down Expand Up @@ -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
Expand Down
89 changes: 86 additions & 3 deletions xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<String> 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<String> 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;
Expand All @@ -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<String, Class<?>> CLASS_CACHE = new ConcurrentHashMap<>();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.Set;

/**
* Created by xuxueli on 17/3/1.
Expand Down Expand Up @@ -52,8 +53,16 @@ public Response<String> 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<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;
import java.util.concurrent.*;

/**
Expand Down Expand Up @@ -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);
Expand All @@ -168,18 +177,20 @@ 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.");
}
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
Expand All @@ -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);
Expand All @@ -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
*/
Expand Down
Loading