在看這一期之前,需要先看上一期的文章:
Java 原生實現代碼沙箱(OJ判題系統第1期)——設計思路、實現步驟、代碼實現-CSDN博客
Java 程序可能出現的異常情況
1、執行超時
占用時間資源,導致程序卡死,不釋放資源:
/*** 無限睡眠(阻塞程序執行)*/
public class SleepError {public static void main(String[] args) throws InterruptedException {long ONE_HOUR = 60 * 60 * 1000L;Thread.sleep(ONE_HOUR);System.out.println("睡完了");}
}
2、占用內存
/*** 無限占用空間(浪費系統內存)*/
public class MemoryError {public static void main(String[] args) throws InterruptedException {List<byte[]> bytes = new ArrayList<>();while (true) {bytes.add(new byte[10000]);}}
}
3、讀文件,文件信息泄露
/*** 讀取服務器文件(文件信息泄露)*/
public class ReadFileError {public static void main(String[] args) throws InterruptedException, IOException {String userDir = System.getProperty("user.dir");String filePath = userDir + File.separator + "src/main/resources/application.yml";List<String> allLines = Files.readAllLines(Paths.get(filePath));System.out.println(String.join("\n", allLines));}
}
4、寫文件,越權植入木馬
/*** 向服務器寫文件(植入危險程序)*/
public class WriteFileError {public static void main(String[] args) throws InterruptedException, IOException {String userDir = System.getProperty("user.dir");String filePath = userDir + File.separator + "src/main/resources/木馬程序.bat";String errorProgram = "java -version 2>&1";Files.write(Paths.get(filePath), Arrays.asList(errorProgram));System.out.println("寫木馬成功,你完了哈哈");}
}
5、運行程序
/*** 運行其他程序(比如危險木馬)*/
public class RunFileError {public static void main(String[] args) throws InterruptedException, IOException {String userDir = System.getProperty("user.dir");String filePath = userDir + File.separator + "src/main/resources/木馬程序.bat";Process process = Runtime.getRuntime().exec(filePath);process.waitFor();// 分批獲取進程的正常輸出BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));// 逐行讀取String compileOutputLine;while ((compileOutputLine = bufferedReader.readLine()) != null) {System.out.println(compileOutputLine);}System.out.println("執行異常程序成功");}
}
Java 程序安全控制
1、超時控制
private static final long TIME_OUT = 5000L; // 定義超時時間為5000毫秒(5秒)// 啟動一個新線程進行超時控制
new Thread(() -> {try {// 讓當前線程休眠TIME_OUT指定的時間Thread.sleep(TIME_OUT);// 超時后打印消息System.out.println("超時了,中斷");// 終止運行中的進程runProcess.destroy();} catch (InterruptedException e) {// 如果線程在休眠期間被中斷,拋出RuntimeExceptionthrow new RuntimeException(e);}
}).start(); // 啟動線程
2、限制資源分配
我們不能讓每個 java 進程的執行占用的 JVM 最大堆內存空間都和系統默認的一致(魚皮的 JVM 默認最大占用 8G 內存),實際上應該更小(執行用戶的題目代碼也不需要這么多),比 如說 256MB。在啟動 Java 程序時,可以指定 JVM 的參數: -Xmx256m(最大堆空間大小) 示例命令如下:
String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
?小知識 - 常用 JVM 啟動參數
1. 內存相關參數:
-Xms
: 設置 JVM 的初始堆內存大小。-Xmx
: 設置 JVM 的最大堆內存大小。-Xss
: 設置每個線程的棧大小。-XX:MaxMetaspaceSize
: 設置 Metaspace(元空間)的最大大小。-XX:MaxDirectMemorySize
: 設置直接內存(Direct Memory)的最大大小。2. 垃圾回收相關參數:
-XX:+UseSerialGC
: 使用串行垃圾回收器。-XX:+UseParallelGC
: 使用并行垃圾回收器。-XX:+UseConcMarkSweepGC
: 使用 CMS(并發標記清除)垃圾回收器。-XX:+UseG1GC
: 使用 G1 垃圾回收器。3. 線程相關參數:
-XX:ParallelGCThreads
: 設置并行垃圾回收使用的線程數。-XX:ConcGCThreads
: 設置并發垃圾回收使用的線程數。-Xss
?或?-XX:ThreadStackSize
: 設置每個線程的棧大小。注意:-Xss
?和?-XX:ThreadStackSize
?是等效的。4. JIT 編譯器相關參數:
-XX:TieredCompilation
: 啟用分層編譯模式。-XX:TieredStopAtLevel
: 設置 JIT 編譯器停止編譯的層次。5. 其他資源限制參數:
-XX:MaxRAM
: 設置 JVM 可以使用的最大物理內存。說明:
-Xss
?和?-XX:ThreadStackSize
?都用于設置線程的棧大小,通常情況下兩者是等效的。-XX:MaxRAM
?參數在較新的 JDK 版本中引入,用于自動配置 JVM 的內存使用。
3、限制代碼 - 黑白名單
先初始化字典樹,插入禁用詞:
private static final List<String> blackList = Arrays.asList("Files", "exec");private static final WordTree WORD_TREE;static {// 初始化字典樹WORD_TREE = new WordTree();WORD_TREE.addWords(blackList);}
?校驗用戶代碼是否包含禁用詞:
// 校驗代碼中是否包含黑名單中的命令FoundWord foundWord = WORD_TREE.matchWord(code);if (foundWord != null) {System.out.println("包含禁止詞:" + foundWord.getFoundWord());return null;}
限制權限 - Java 安全管理器
Java 安全管理器(Security Manager)是 Java 提供的保護 JVM、Java 安全的機制,可以實 現更嚴格的資源和操作限制。
?1.所有權限放開
/*** 默認安全管理器*/
public class DefaultSecurityManager extends SecurityManager {// 檢查所有的權限@Overridepublic void checkPermission(Permission perm) {System.out.println("默認不做任何限制");System.out.println(perm);
// super.checkPermission(perm);}
}
2.所有權限拒絕
/*** 禁用所有權限安全管理器*/
public class DenySecurityManager extends SecurityManager {// 檢查所有的權限@Overridepublic void checkPermission(Permission perm) {throw new SecurityException("權限異常:" + perm.toString());}
}
3.限制權限
public class MySecurityManager extends SecurityManager {// 檢查所有的權限@Overridepublic void checkPermission(Permission perm) {
// super.checkPermission(perm);}// 檢測程序是否可執行文件@Overridepublic void checkExec(String cmd) {throw new SecurityException("checkExec 權限異常:" + cmd);}// 檢測程序是否允許讀文件@Overridepublic void checkRead(String file) {System.out.println(file);if (file.contains("C:\\code\\yuoj-code-sandbox")) {return;}
// throw new SecurityException("checkRead 權限異常:" + file);}// 檢測程序是否允許寫文件@Overridepublic void checkWrite(String file) {
// throw new SecurityException("checkWrite 權限異常:" + file);}// 檢測程序是否允許刪除文件@Overridepublic void checkDelete(String file) {
// throw new SecurityException("checkDelete 權限異常:" + file);}// 檢測程序是否允許連接網絡@Overridepublic void checkConnect(String host, int port) {
// throw new SecurityException("checkConnect 權限異常:" + host + ":" + port);}
}
測試:
/*** 測試安全管理器*/
public class TestSecurityManager {public static void main(String[] args) {System.setSecurityManager(new MySecurityManager());FileUtil.writeString("aa", "aaa", Charset.defaultCharset());}
}
在運行java程序時,指定安全管理器的路徑、安全管理器的名稱:
private static final String SECURITY_MANAGER_PATH = "C:\\code\\code-sandbox\\src\\main\\resources\\security";private static final String SECURITY_MANAGER_CLASS_NAME = "MySecurityManager";String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s;%s -Djava.security.manager=%s Main %s", userCodeParentPath, SECURITY_MANAGER_PATH, SECURITY_MANAGER_CLASS_NAME, inputArgs);
至此,第二期結束,但是安全管理器有一定缺點:
1. 如果要做比較嚴格的權限限制,需要自己去判斷哪些文件、包名需要允許讀寫。粒度太細 了,難以精細化控制。
2. 安全管理器本身也是 Java 代碼,也有可能存在漏洞。本質上還是程序層面的限制,沒深 入系統的層面。
所以下一期我們來講一下,代碼沙箱 Docker 實現
?