如何在Java Rhino安全Context中指定允许访问的Java类?
我明白你想要的是一个严格受控的JavaScript沙箱——用Context.initSafeStandardObjects()打下安全基础,但又得让JS能访问你指定的几个Java类,绝对不能像initStandardObjects()那样开放全部权限。这在Rhino的沙箱场景里是个很典型的需求,我来给你拆解下具体怎么做。
核心思路
initSafeStandardObjects()会生成一个默认屏蔽所有Java类访问权限的全局对象,我们的目标就是手动把你指定的Java类/方法添加到这个全局对象中,同时通过额外的拦截机制,确保其他未授权的类完全无法被访问。
具体实现步骤
1. 初始化安全全局对象
先通过initSafeStandardObjects()创建上下文和全局作用域,这是保证沙箱基础安全的第一步:
Context cx = Context.enter(); try { Scriptable scope = cx.initSafeStandardObjects(); // 后续添加指定类的操作都在这里进行 } finally { Context.exit(); }
2. 向全局作用域暴露指定Java类
要让JS能访问Java类,需要用Context.javaToJS()把Java类/对象转换成Scriptable类型,再挂载到全局作用域里。这里有两种常见的实现方式,安全性和灵活性各有侧重:
方式一:直接暴露单个Java类
如果你确定某个类的所有方法都可以开放,直接把类本身挂载到全局作用域即可。比如允许JS访问java.util.ArrayList:
// 获取要暴露的目标Java类 Class<?> arrayListClass = java.util.ArrayList.class; // 转换为JS可识别的对象 Scriptable arrayListObj = cx.javaToJS(arrayListClass, scope); // 挂载到全局作用域,JS中可通过`ArrayList`直接调用 scope.put("ArrayList", scope, arrayListObj);
对应的JS使用示例:
let myList = new ArrayList(); myList.add("hello sandbox"); print(myList.size()); // 输出1
方式二:通过包装类暴露有限功能
如果不想开放类的全部方法(比如避免危险的getClass()等反射入口),最好自定义一个包装类,只暴露你需要的功能,再把包装类的实例挂载到全局作用域:
// 自定义安全包装类,仅开放必要方法 public class SafeListWrapper { private ArrayList<String> innerList = new ArrayList<>(); public void addItem(String item) { innerList.add(item); } public int getCount() { return innerList.size(); } } // 在沙箱中暴露包装类实例 SafeListWrapper safeList = new SafeListWrapper(); Scriptable safeListObj = cx.javaToJS(safeList, scope); scope.put("SafeList", scope, safeListObj);
对应的JS使用示例:
SafeList.addItem("controlled access"); print(SafeList.getCount()); // 输出1
这种方式能最大程度缩小攻击面,是更安全的选择。
3. 用ClassShutter彻底拦截未授权类
即使你只暴露了指定类,JS仍可能通过反射绕过限制。这时需要用Context.setClassShutter()设置一个拦截器,只有你明确授权的类才能被访问:
cx.setClassShutter(new ClassShutter() { @Override public boolean visibleToScripts(String className) { // 仅允许以下类被JS访问 return className.equals("java.util.ArrayList") || className.equals("com.yourpackage.SafeListWrapper"); } });
这个拦截器会检查所有JS对Java类的访问请求,只有返回true的类才能被加载,彻底杜绝反射绕过的可能。
完整示例代码
把上述步骤整合起来,一个完整的受控沙箱实现如下:
import org.mozilla.javascript.*; public class ControlledJsSandbox { public static void main(String[] args) { Context cx = Context.enter(); try { // 设置JS语言版本为ES5,保证兼容性 cx.setLanguageVersion(Context.VERSION_ES5); // 初始化安全全局对象 Scriptable scope = cx.initSafeStandardObjects(); // 设置类访问拦截器 cx.setClassShutter(new ClassShutter() { @Override public boolean visibleToScripts(String className) { return className.equals("java.util.ArrayList") || className.equals("com.yourpackage.SafeListWrapper"); } }); // 暴露指定Java类到全局作用域 Class<?> arrayListClass = java.util.ArrayList.class; Scriptable arrayListObj = cx.javaToJS(arrayListClass, scope); scope.put("ArrayList", scope, arrayListObj); // 测试沙箱内的JS代码 String jsCode = "let list = new ArrayList(); list.add('test'); print(list.size());"; cx.evaluateString(scope, jsCode, "SandboxTest", 1, null); } finally { Context.exit(); } } }
关键安全注意事项
- 绝对不要暴露
java.lang.Class、java.lang.reflect相关类,否则JS可以通过反射获取任何系统类。 - 优先使用包装类暴露功能,而非直接开放原生Java类,减少不必要的权限。
- 测试时要尝试各种绕过手段(比如通过暴露类的方法间接获取其他类实例),确保沙箱没有漏洞。
内容的提问来源于stack exchange,提问作者František Jeřábek

