知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 09 - 安全沙箱 > 这篇解决什么问题:你要在 Java 里执行一段"不一定可信"的 JS——用户提交的规则、报表表达式、插件脚本,甚至是从外部灌进来的字符串。让它裸奔,脚本一坏就能读 `/etc/passwd`、写敏感文件、起子进程反弹 shell、把你注入的 Java 对象玩坏。但你也不能因此就不敢用脚本引擎。关键问题是:怎么把它"关进笼子里",让它只做纯计算、其他统统碰不到? > 读完你会知道:GraalJS 沙箱的底层思路为什么是"默认全禁、按需打开"、`allow*` 这套开关各自钳制哪一类危险能力、一个"只留纯计算、其余全关"的最严格 Context 长什么样、为什么沙箱里 `Java.type`/文件访问/`require('fs')` 全被拒,以及"沙箱不是万能"——它挡不住哪些攻击、还需要叠加什么。 --- ## 1. 先建立安全心智:最小权限原则 ### 1.1 为什么要用"默认全禁"而不是"默认放行" 写普通 Java 时,代码天生拥有 JVM 的一切权限——读写文件、开线程、起进程、加载类。这对你自己的代码没问题,因为**你信任它**。但一段来自用户输入或运营配置的 JS,本质是**不可信代码**,它和你的信任假设之间隔着一条鸿沟。 GraalJS 的沙箱策略和它相反:**默认全禁,按需打开。** 这不是靠"列举该禁什么",而是把所有能力默认关掉,要哪项、开哪项。用大白话说: - **自己的代码**(`Context.create`)→ 默认配置,纯计算可用 - **半可信代码**(需要某业务能力)→ 只打开对应的 `allow*` 开关 - **不可信代码**(用户提交)→ 全关,最小权限 > 一句话:**沙箱的心智是"什么都没给"起步,而不是"给了再收"——真要做坏事的能力,一开始就不该存在。** ### 1.2 权限矩阵:每一个 `allow*` 开关钳的是哪条"做坏事"的路 下面这张矩阵是这一篇的核心。它回答"为什么关了几个开关,脚本就变安全了"——因为脚本做坏事的所有通道,都被逐一封死: ``` ┌──────────────────────────────────────────────────┐ │ 沙箱权限控制矩阵 │ │ │ │ 权限 默认 推荐(沙箱) │ │ ───────────────────────────────────────────── │ │ allowIO NONE NONE │ │ allowHostAccess NONE NONE / SCOPED │ │ allowHostClassLookup false false │ │ allowCreateThread false false │ │ allowNativeAccess false false │ │ allowCreateProcess false false │ │ allowEnvironmentAccess NONE NONE │ │ │ │ SCOPED = 只暴露特定的 Java 类/方法给 JS │ └──────────────────────────────────────────────────┘ ``` 逐项拆开,说清楚每个开关到底拦住什么、不开会怎样: | 开关 | 默认 | 控制什么 | 危险例子 | |---|---|---|---| | `allowIO(IOAccess.NONE)` | NONE | 文件系统/网络读写 | 读 `/etc/passwd`、写敏感文件 | | `allowHostAccess(HostAccess.NONE)` | NONE | 调用 Java 对象方法 | 调用注入的 Java 对象做越权操作 | | `allowHostClassLookup(s -> false)` | false | `Java.type()` 查 Java 类 | 反射式攻击、加载任意类 | | `allowCreateThread(false)` | false | 创建线程 | 线程轰炸/资源耗尽 | | `allowNativeAccess(false)` | false | 调用 native 代码 | 绕过 JVM 安全机制 | | `allowCreateProcess(false)` | false | 启动子进程 | `rm -rf`、反弹 shell | | `allowEnvironmentAccess` | NONE | 读取环境变量 | 窃取密钥等敏感配置 | **这七条就是脚本做坏事的全部常见入口。** 你可以这样理解它们的合力:想让脚本"只有脑子、没有手脚"——能算(纯运算),但不能接、不能写、不能启进程、不能碰类、不能动环境。剩下唯一要担心的,是它不停计算消耗 CPU/内存(后文专门讲)。 下面这张图把"默认全禁、按需打开"的最小权限沙箱思路画出来:  *图:最小权限沙箱:默认全禁,按需打开* > 一句话:**IO、宿主访问、类查找、线程、native、子进程、环境变量——七扇"做坏事"的门全部关上,脚本就只剩纯计算这一条活路。** --- ## 2. 一个"最严格沙箱"长什么样:逐行拆 ### 2.1 从 07 篇的"全放行"到这里"全关掉" 如果你看过前面讲宿主访问(`HostAccess.ALL` + `lookup(s -> true)`)的篇目,这里就是它的**正面镜像**——把每一项都收到最紧: ```java try (Context context = Context.newBuilder("js") .allowIO(IOAccess.NONE) .allowHostAccess(HostAccess.NONE) .allowHostClassLookup(s -> false) .allowCreateThread(false) .allowNativeAccess(false) .allowCreateProcess(false) .option("engine.WarnInterpreterOnly", "false") .build()) { // ... 在这个 context 里执行脚本 } ``` 逐行看每个开关在干嘛: - `allowIO(IOAccess.NONE)`:彻底关死文件与网络 IO。 - `allowHostAccess(HostAccess.NONE)`:脚本拿不到任何 Java 对象的方法。 - `allowHostClassLookup(s -> false)`:`lambda` 恒返回 `false`,等于禁止一切 `Java.type()` 类查找。 - `allowCreateThread(false)` / `allowNativeAccess(false)` / `allowCreateProcess(false)`:线程、native、子进程三路全关。 - `option("engine.WarnInterpreterOnly", "false")`:关掉"仅解释执行"的警告输出——当运行在纯 JDK 上、Truffle 无法 JIT 时,引擎会打一条提示,这里选择静音它。 > 一句话:**最严格沙箱 = 七个开关全关 + 一条可选的"解释执行"提示静音,Context 做成一只"只能算"的铁笼。** ### 2.2 实测:什么能过、什么被拒 **运行结果(实测演示):** ``` 纯计算 Math.sqrt(144) = 12.0 尝试 Java.type -> 被拒绝: <错误消息,如 Java.type is not defined 等> 尝试访问文件系统 -> 被拒绝: <错误消息> 尝试 require('fs') -> 被拒绝: <错误消息> ``` (后三行的具体消息被示例的 `truncate` 方法截断到 100 字符,为了控制台整洁。) #### ① 纯计算——放行 ```java Value sqrt = context.eval("js", "Math.sqrt(144)"); System.out.println("纯计算 Math.sqrt(144) = " + sqrt.asDouble()); // 纯计算 Math.sqrt(144) = 12.0 ``` `Math.sqrt` 是 JS 内置数学函数,不触碰任何系统资源,沙箱自然放行。这印证了沙箱的定位:**不是"不能用引擎",而是"只用它的计算能力"。** #### ② 尝试访问 Java 类——被拒 ```java try { context.eval("js", "Java.type('java.lang.System')"); } catch (PolyglotException e) { System.out.println("尝试 Java.type -> 被拒绝: " + truncate(e.getMessage())); } ``` 沙箱下连 `Java` 这个全局对象都不存在了,`Java.type(...)` 直接抛 `PolyglotException`。注意这里就用到 08 篇学的异常处理——脚本尝试越界,冒出的仍然是你熟悉的那个异常类型。 #### ③ 尝试访问文件系统——被拒 ```java try { context.eval("js", """ const fs = Java.type('java.nio.file.Files'); fs.readString(Path.of('/etc/passwd')); """); } catch (PolyglotException e) { System.out.println("尝试访问文件系统 -> 被拒绝: " + truncate(e.getMessage())); } ``` 即使脚本想绕个弯:先 `Java.type('java.nio.file.Files')` 拿到文件类,再读 `/etc/passwd`——第一行 `Java.type` 就被类查找开关拦下了(而且 `Path.of` 里的 `Path` 在沙箱里根本没有定义),整个攻击链在第一环就断了。**这个例子很关键:读文件要靠"拿类"这条线,而"拿类"早被关了,所以它一败到底。** #### ④ 尝试加载 Node 模块——被拒 ```java try { context.eval("js", "require('fs')"); } catch (PolyglotException e) { System.out.println("尝试 require('fs') -> 被拒绝: " + truncate(e.getMessage())); } ``` `require`(CommonJS 加载器)在未开启对应选项时不可用,`require('fs')` 抛异常。JS 侧不再拥有"拉任意模块"的能力,相当于把社区库路径也封死了。 > 一句话:**"能放过纯计算(Math.sqrt),全拒越界(Java.type / 文件 / require)"——一次运行,把沙箱的"能/不能"边界验证得明明白白。** --- ## 3. 沙箱到底有多安全?——诚实面对两个边界 ### 3.1 它挡不住的"资源耗尽"与"逻辑漏洞" 沙箱不是魔法,它防的是"权限越界",而不是"恶意算法"。即便脚本一个系统资源都碰不到,仍然有两类攻击路径: - **CPU/内存耗尽**:`while(true){}` 无限循环、构造超大数组、深递归,都能拖垮你这个 JVM 进程。 - **逻辑漏洞**:即使脚本拿不到任何系统资源,它也可能在**你注入的数据**上做意外操作——比如规则引擎里对 facts 对象的越权改写,这不算沙箱漏洞,但同样是"业务伤害"。 GraalJS 为第一类提供超时类的能力,比如可通过 `Context.Builder.option("js.engine-timeout", ...)` 一类的选项设置执行超时,配合资源限制使用。这是"软补丁"——纯靠开关挡不住,得靠"超时/配额"这类计量手段。 ### 3.2 最佳实践:三层叠加,而不是"开箱即安全" 正因为沙箱只防权限、不防算法,正确姿势是**三层叠加**: ``` 第一层:最小权限沙箱(本篇) → 封死 IO/类查找/线程/进程/native/环境变量 第二层:超时与资源限制 → 防止无限循环拖垮 CPU / 内存耗尽 第三层:输入校验 → 业务数据本身也要先验过再喂给脚本 ``` 只用第一层就敢对外开,是不负责任的。后面规则引擎篇(第 11 篇)会用这套三层思路做一个真实落地。 > 一句话:**沙箱解决"权限越界",不解决"恶意算法"——完整方案 = 最小权限 + 超时/资源限制 + 输入校验,三层叠加。** --- ## 4. 一个工具细节:`truncate` 是干嘛的 示例里的 `truncate` 是一个私有工具方法,跟沙箱无关、只为了输出整洁——把超长异常消息截断到 100 字符并加省略号,防止控制台被一屏一屏的堆栈淹没: ```java private static String truncate(String s) { return s.length() > 100 ? s.substring(0, 100) + "..." : s; } ``` > 一句话:**`truncate` 只是"截断输出"的一个小工具,跟沙箱要守护的东西没有关系。** --- ## 5. 本篇小结 ```text ┌──────────────────────────────────────────────────────┐ │ 09 安全沙箱 要带走的话 │ ├──────────────────────────────────────────────────────┤ │ 1. 心智:默认全禁,按需打开(不是放行后再收) │ │ 2. 七扇门全关:IO/宿主/类查找/线程/native/进程/环境 │ │ 3. 只留纯计算:Math.sqrt 过,Java.type 被拒 │ │ 4. 沙箱只防权限越界,不防无限循环(CPU 耗尽) │ │ 5. 完整方案要三层:沙箱 + 超时/资源 + 输入校验 │ └──────────────────────────────────────────────────────┘ │ v 你已经能把不可信脚本关进一只"只能算"的铁笼 ──→ 下一步:把它用进规则引擎实战 ``` > 一句话收束:**沙箱不是"禁用脚本",而是把脚本的能力收敛成"只能算"——然后靠超时和校验补齐其余风险。** ## 思考题(自测) 1. `allowHostClassLookup(s -> false)` 和 `s -> true` 的两个 lambda,分别代表什么策略?能否改成一个"只允许查 `java.util.*`"的白名单(提示:写在 lambda 的 `s` 参数里过滤)? 2. 如果业务确实需要 JS 读某个指定目录下的模板文件,沙箱该开哪个开关、又该怎么把范围限定到那一个目录而不是全盘开放? 3. 一个 `while(true){}` 无限循环在沙箱里会怎样?你会在哪一层加超时——`Context` 的选项,还是外层再用一个线程做 watchdog?各自的取舍是什么?
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入