private ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
//获得JavaScript脚本引擎
private ScriptEngine jsEngine = scriptEngineManager.getEngineByName("js");
@GetMapping("wrong")
public Object wrong(@RequestParam("name") String name) {
try {
//通过eval动态执行JavaScript脚本,这里name参数通过字符串拼接方式混入JavaScript代码
return jsEngine.eval(String.format("var name='%s'; name=='admin'?1:0;", name));
} catch (ScriptException e) {
e.printStackTrace();
}
return null;
}
但是假如我们传入的name参数是 haha';java.lang.System.exit(0);' 就可以达到关闭整个程序的目的。原因是,我们直接把代码和数据拼接在了一起。外部如 果构造了一个特殊的用户名先闭合字符串的单引号,再执行一条 System.exit 命令的话,就可以满足脚本不出错,命令被执行
解决方法1:
@GetMapping("right")
public Object right(@RequestParam("name") String name) {
try {
//外部传入的参数
Map<String, Object> parm = new HashMap<>();
parm.put("name", name);
//name参数作为绑定传给eval方法,而不是拼接JavaScript代码
return jsEngine.eval("name=='admin'?1:0;", new SimpleBindings(parm));
} catch (ScriptException e) {
e.printStackTrace();
}
return null;
}
解决方法2: 使用 SecurityManager 配合 AccessControlContext,来构建一个脚本运行的沙箱环境。脚本能执行的所有操作权限,是通过 setPermissions 方法精细化设置 的:
@Slf4j
public class ScriptingSandbox {
private ScriptEngine scriptEngine;
private AccessControlContext accessControlContext;
private SecurityManager securityManager;
private static ThreadLocal<Boolean> needCheck = ThreadLocal.withInitial(() -> false);
public ScriptingSandbox(ScriptEngine scriptEngine) throws InstantiationException {
this.scriptEngine = scriptEngine;
securityManager = new SecurityManager(){
//仅在需要的时候检查权限
@Override
public void checkPermission(Permission perm) {
if (needCheck.get() && accessControlContext != null) {
super.checkPermission(perm, accessControlContext);
}
}
};
//设置执行脚本需要的权限
setPermissions(Arrays.asList(
new RuntimePermission("getProtectionDomain"),
new PropertyPermission("jdk.internal.lambda.dumpProxyClasses","read"),
new FilePermission(Shell.class.getProtectionDomain().getPermissions().elements().nextElement().getName(),"read"),
new RuntimePermission("createClassLoader"),
new RuntimePermission("accessClassInPackage.jdk.internal.org.objectweb.*"),
new RuntimePermission("accessClassInPackage.jdk.nashorn.internal.*"),
new RuntimePermission("accessDeclaredMembers"),
new ReflectPermission("suppressAccessChecks")
));
}
//设置执行上下文的权限
public void setPermissions(List<Permission> permissionCollection) {
Permissions perms = new Permissions();
if (permissionCollection != null) {
for (Permission p : permissionCollection) {
perms.add(p);
}
}
ProtectionDomain domain = new ProtectionDomain(new CodeSource(null, (CodeSigner[]) null), perms);
accessControlContext = new AccessControlContext(new ProtectionDomain[]{domain});
}
public Object eval(final String code) {
SecurityManager oldSecurityManager = System.getSecurityManager();
System.setSecurityManager(securityManager);
needCheck.set(true);
try {
//在AccessController的保护下执行脚本
return AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
try {
return scriptEngine.eval(code);
} catch (ScriptException e) {
e.printStackTrace();
}
return null;
}, accessControlContext);
} catch (Exception ex) {
log.error("抱歉,无法执行脚本 {}", code, ex);
} finally {
needCheck.set(false);
System.setSecurityManager(oldSecurityManager);
}
return null;
}
@GetMapping("right2")
public Object right2(@RequestParam("name") String name) throws InstantiationException {
//使用沙箱执行脚本
ScriptingSandbox scriptingSandbox = new ScriptingSandbox(jsEngine);
return scriptingSandbox.eval(String.format("var name='%s'; name=='admin'?1:0;", name));
}
再次请求这个地址 http://localhost:45678/codeinject/right2?name=haha%27;java.lang.System.exit(0);%27
我们发现沙箱生效了 [13:09:36.080] [http-nio-45678-exec-1] [ERROR] [o.g.t.c.c.codeinject.ScriptingSandbox:77 ] - 抱歉,无法执行脚本 var name='haha';java.lang.System.exit(0);''; name=='admin'?1:0; java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "exitVM.0") at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472) at java.lang.SecurityManager.checkPermission(SecurityManager.java:585) at org.geekbang.time.commonmistakes.codeanddata.codeinject.ScriptingSandbox$1.checkPermission(ScriptingSandbox.java:30) at java.lang.SecurityManager.checkExit(SecurityManager.java:761) at java.lang.Runtime.exit(Runtime.java:107)