OJ判题项目-原生代码沙箱(五)
往期开发(已经完成)
- OJ 判题项目-介绍(一)
- OJ 判题项目-初始化(二)
- OJ 判题项目-前后端联调(三)
- OJ 判题项目-简易判题机(四)
- OJ 判题项目-原生代码沙箱(五)
- OJ 判题项目-Docker 代码沙箱(六)
后端开发
代码沙箱项目初始化
代码沙箱定位:只负责接收代码和测试用例运行代码返回执行结果,不负责执行结果的正确性,主要实现 JAVA 代码沙箱,其他的相同
创建独立的 springboot-web 项目 使用 IDEA 的 spring 初始化器
然后编写测试接口启动项目测试即可
代码沙箱简易执行流程
- 接收代码和测试用例以及用户的提交信息
- 根据语言进行编译代码,生成对应可执行文件
- 然后使用命令行的方式进行执行代码例如 java 的 javac 编译,java -cp 执行代码
模拟代码沙箱的执行流程
分析代码沙箱执行规范(JAVA)
由于 java 原生执行代码需要指定类名传参
所以要先保证类名的一致统一类名为 Main原生核心流程梳理
主要实现思路:编写一套程序流程,当接口接收到请求之后,自动的去执行这些命令进行编译,运行,提取结果
使用 jdk 中自带的进程类 Process 实现- 将用户代码保存再进行编译得到 class 文件
- 执行代码得到输出结果
- 收集整理输出结果
- 文件清理,执行完成之后清除 class 文件防止磁盘占用
- 进行异常处理
原生代码沙箱实现
保存用户代码并编译代码文件生成 class 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// 获取用户的工作目录
String userDir = System.getProperty("user.dir");
// 创建全局存储代码目录
String globalCodePathName = userDir + File.separator + GLOBAL_CODE_FILE_NAME;
// 没有的话创建目录
if (!FileUtil.exist(globalCodePathName)) {
FileUtil.mkdir(globalCodePathName);
}
// 创建用户单个目录存储代码文件 实现每个用户的代码隔离
String userCodeFileParentPath = globalCodePathName + File.separator + UUID.randomUUID();
// 创建用户的代码文件
String userCodeFilePath = userCodeFileParentPath + File.separator + GLOBAL_CLASS_NAME;
// 写入代码
File userCodeFile = FileUtil.writeString(code, userCodeFilePath, StandardCharsets.UTF_8);
try{
// 1. 构建编译指令
String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
// 2. 执行编译命令 得到结果
Process compileProcess = Runtime.getRuntime().exec(compileCmd);
ExecuteMessage compileExecuteMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
System.out.println("编译的结果为:" + compileExecuteMessage);
if (!StrUtil.isEmpty(compileExecuteMessage.getErrorMessage())) {
return ProcessUtils.getErrorExecuteCodeResponse(compileExecuteMessage);
}
} catch (Exception e){
e.printStackTrace();
}执行代码文件得到输出结果
由于是多组输出结果要进行循环遍历每组用例分别执行并收集结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19List<ExecuteMessage> runExecuteMessageList = new ArrayList<>(inputList.size());
long runMaxTime = 0L;
try{
for (String args : inputList) {
// 1. 构建执行程序命令
String runCmd = String.format("java -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeFileParentPath, args);
// 2. 执行运行命令 得到结果
Process runProcess = Runtime.getRuntime().exec(runCmd);
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ExecuteMessage runExecuteMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
stopWatch.stop();
runMaxTime = Math.max(stopWatch.getLastTaskTimeMillis(), runMaxTime);
System.out.println("运行结果为:" + runExecuteMessage);
runExecuteMessageList.add(runExecuteMessage);
}
} catch (Exception e){
e.printStackTrace();
}整理输出结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21List<String> outputList = new ArrayList<>();
for (ExecuteMessage executeMessage : runExecuteMessageList) {
if (!StrUtil.isEmpty(compileExecuteMessage.getErrorMessage())) {
return ProcessUtils.getErrorExecuteCodeResponse(executeMessage);
} else {
outputList.add(executeMessage.getMessage());
}
}
if (outputList.size() != runExecuteMessageList.size()) {
executeCodeResponse.setMessage("结果数量不符合");
return executeCodeResponse;
}
// 封装返回结果
executeCodeResponse.setOutputList(outputList);
executeCodeResponse.setMessage("运行成功");
executeCodeResponse.setStatus(0);
JudgeInfo judgeInfo = new JudgeInfo();
judgeInfo.setMessage("结果正确");
judgeInfo.setTime(runMaxTime);
judgeInfo.setMemory(0L);
executeCodeResponse.setJudgeInfo(new JudgeInfo());清理文件
1
2
3
4
5// 代码执行完毕之后 清除文件
if (userCodeFile.getParentFile() != null) {
boolean del = FileUtil.del(userCodeFileParentPath);
System.out.println("删除" + (del ? "成功" : "失败"));
}全过程代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74public static ExecuteCodeResponse doJudge(ExecuteCodeRequest executeCodeRequest) {
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
List<String> inputList = executeCodeRequest.getInputList();
String code = executeCodeRequest.getCode();
// 获取用户的工作目录
String userDir = System.getProperty("user.dir");
// 创建全局存储代码目录
String globalCodePathName = userDir + File.separator + GLOBAL_CODE_FILE_NAME;
// 没有的话创建目录
if (!FileUtil.exist(globalCodePathName)) {
FileUtil.mkdir(globalCodePathName);
}
// 创建用户单个目录存储代码文件 实现每个用户的代码隔离
String userCodeFileParentPath = globalCodePathName + File.separator + UUID.randomUUID();
// 创建用户的代码文件
String userCodeFilePath = userCodeFileParentPath + File.separator + GLOBAL_CLASS_NAME;
// 写入代码
File userCodeFile = FileUtil.writeString(code, userCodeFilePath, StandardCharsets.UTF_8);
try {
// 1. 构建编译指令
String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
// 2. 执行编译命令 得到结果
Process compileProcess = Runtime.getRuntime().exec(compileCmd);
ExecuteMessage compileExecuteMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
System.out.println("编译的结果为:" + compileExecuteMessage);
if (!StrUtil.isEmpty(compileExecuteMessage.getErrorMessage())) {
return ProcessUtils.getErrorExecuteCodeResponse(compileExecuteMessage);
}
List<ExecuteMessage> runExecuteMessageList = new ArrayList<>(inputList.size());
long runMaxTime = 0L;
for (String args : inputList) {
// 1. 构建执行程序命令
String runCmd = String.format("java -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeFileParentPath, args);
// 2. 执行运行命令 得到结果
Process runProcess = Runtime.getRuntime().exec(runCmd);
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ExecuteMessage runExecuteMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
stopWatch.stop();
runMaxTime = Math.max(stopWatch.getLastTaskTimeMillis(), runMaxTime);
System.out.println("运行结果为:" + runExecuteMessage);
runExecuteMessageList.add(runExecuteMessage);
}
List<String> outputList = new ArrayList<>();
for (ExecuteMessage executeMessage : runExecuteMessageList) {
if (!StrUtil.isEmpty(compileExecuteMessage.getErrorMessage())) {
return ProcessUtils.getErrorExecuteCodeResponse(executeMessage);
} else {
outputList.add(executeMessage.getMessage());
}
}
if (outputList.size() != runExecuteMessageList.size()) {
executeCodeResponse.setMessage("结果数量不符合");
return executeCodeResponse;
}
// 封装返回结果
executeCodeResponse.setOutputList(outputList);
executeCodeResponse.setMessage("运行成功");
executeCodeResponse.setStatus(0);
JudgeInfo judgeInfo = new JudgeInfo();
judgeInfo.setMessage("结果正确");
judgeInfo.setTime(runMaxTime);
judgeInfo.setMemory(0L);
executeCodeResponse.setJudgeInfo(new JudgeInfo());
// 代码执行完毕之后 清除文件
if (userCodeFile.getParentFile() != null) {
boolean del = FileUtil.del(userCodeFileParentPath);
System.out.println("删除" + (del ? "成功" : "失败"));
}
} catch (Exception e) {
e.printStackTrace();
}
return executeCodeResponse;
}
执行异常情况
- 执行超时,设置过期时间即可,或者是守护线程,时间超时自动中断
- 内存占用,导致空间浪费,JVM 有保护机制来处理内存溢出情况 OOM 机制 可以使用 JConsole 工具链接到 JVM 虚拟机查看
- 读写文件,信息泄露,通过代码程序来获取服务的配置文件,植入木马等危险操作
- 运行其他程序,指定目录来运行服务中的一些代码文件
- 执行高位操作,编写 bat 文件执行 shell 命令
java 程序安全控制
超时控制
1
2
3
4
5
6
7
8
9
10// 守护线程
new Thread(() -> {
try {
Thread.sleep(TIME_OUT);
System.out.println("超时了,中断");
runProcess.destroy();
} catch (Exception e) {
throw new RuntimeException(e);
}
});通过命令行来指定 JVM 最大堆内存,也可以使用 cgroup
1
java -Xmx256m
限制某些代码不可执行(字典树)
但是无法遍历所有的黑名单1
2
3
4
5
6
7
8
9
10
11
12
13
14private static final WordTree WORD_TREE;
private static final List<String> BLACK_LIST = Arrays.asList("Files", "exec");
static {
WORD_TREE = new WordTree();
WORD_TREE.addWord(BLACK_LIST.toString());
}
// 审核代码
FoundWord foundWord = WORD_TREE.matchWord(code);
if (foundWord != null) {
System.out.println("包含敏感词:" + foundWord);
return null;
}限制权限-java 安全管理器
直接限制用户对文件、内存、CPU、网络等资源的权限Java 安全管理器的使用
Java 安全管理器(Security Manager)是 Java 提供的保护 JVM、Java 安全的机制,可以实现更严格的资源和操作限制。
编写安全管理器,只需要继承 SecurityManager 即可打开所有权限
1
2
3
4
5
6
7
8public class DefaultSecurityManager extends SecurityManager {
public void checkPermission(Permission perm) {
// 调用父级方法限制权限 不调用则做任何限制
super.checkPermission(perm);
}
}限制读权限
1
2
3
4
public void checkRead(String file) {
super.checkRead(file);
}- 限制写权限
1
2
3
4
public void checkWrite(String file) {
super.checkWrite(file);
} - 限制执行文件权限
1
2
3
4
public void checkExec(String cmd) {
super.checkExec(cmd);
} - 限制网络链接权限
1
2
3
4
public void checkConnect(String host, int port) {
super.checkConnect(host, port);
} 执行 java 命令时需要带上安全管理器的文件路径
1
java -Dfile.encoding=UTF-8 -cp %s;%s -Djava.security.manager=${路径} ${运行程序}
Java 安全管理器的缺点
如果要做比较严格的权限限制,需要自己去判断那些文件、包名需要允许读写。粒度太细了,权限限制的太死了难以去控制他
安全管理器本身也是代码,也有可能存在漏洞。本质上还是程序层面的限制,没深入系统的层面。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 keep初心!
