项目中的安全问题及解决方法-----动态脚本注入

文章讲述了在Java中如何通过`eval`动态执行JavaScript代码,以及存在的代码注入风险。随后介绍了两种解决方案:一是使用参数绑定而非字符串拼接,二是创建一个带有安全限制的沙箱环境(Sandbox),通过`SecurityManager`和`AccessControlContext`来防止恶意代码执行。
摘要由优快云通过智能技术生成
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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ADRU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值