业务系统提供了一个插件功能,用户可以根据插件规范来编写插件,然后将其制作为Jar包上传到系统,系统将会运行相关函数。另外就是插件Jar包必须和业务系统运行在同一个JVM虚拟机。这时为了避免任意代码执行漏洞,通常可采用如下两种方案:

1、给插件包添加签名校验(需保证插件包无法伪造),或给该功能添加二次认证等

2、当由于业务的特殊性(如不能影响用户体验等),无法使用上述方案时,就需要对jar包的内容进行合法性校验了,这时就可以使用SecurityManage机制了。

SecurityManager概述

SecurityManager是Java内置的一种安全机制,默认处于禁用状态。

其生卒年为:Java1.0 - Java16。

之所以SecurityManager只活到了16岁,是因为SecurityManager最主要的作用就是保护Java Applet应用,随着Java17完全移除了Applet相关API,SecurityManager的使命也完成了,随即也就光荣下岗了。

虽然SecurityManager的生命永远定格在了16岁,但是当前市面上主流的Java版本还是8和11,因此我们还是有必要学习一下SecurityManager,因为他确实可以帮助我们解决一些很棘手的问题。

SecurityManager使用

1、开启SecurityManager沙箱机制

有如下两种开启沙箱机制的方法:

(1)代码启动

实例化一个 java.lang.SecurityManager 或继承它的子类的对象,然后通过 System.setSecurityManager() 来设置启动安全管理器,如下:

System.setSecurityManager(new SecurityManager());

(2)命令启动(建议使用)

该方法无需修改源码,只需要启动Java应用时,添加如下JVM参数即可:

-Djava.security.manager

2、配置安全策略文件

(1)未显式声明策略文件位置时,系统将使用如下默认路径中的策略文件:

java.home/jre/lib/security/java.policy 或user.home/.java.policy

当然也可通过如下JVM参数指定自定义策略文件位置:

-Djava.security.policy=/etc/custom.policy

PS:想进一步了解策略文件详细介绍的道友,可移步官网了解:

https://docs.oracle.com/javase/8/docs/technotes/guides/security/PolicyFiles.html

(2)在指定的策略文件中进行如下配置,即给系统默认的保护域(ProtectionDomain)赋予所有权限:

grant { permission java.security.AllPermission;};

解释如下:
(1)ProtectionDomain由代码源和一组权限集合组成,表示给指定的代码源授予一组权限。当不指定代码源时,默认表示所有代码。

(2)SecurityManager无法在局部开启,也就是说该机制要么不开,要么开了以后将会全局生效;并且SecurityManager采用的是白名单机制,默认情况下,代码源是没有任何权限的。因此为了不影响主体程序的运行,这里我们需要给所有代码赋予所有权限。

(3)这样配置以后,后续就可以通过SecurityManager给局部的Jar包或class文件做权限控制了。

3、自定义ClassLoader

那问题来了,我们如何给局部Jar包或class文件做单独的权限控制呢,经过小胖的不懈努力,终于找到了解决方法。

就是通过自定义ClassLoader,在调用defineClass()方法时,可以给当前加载的类设置ProtectionDomain:

这样就完美解决了,自定义ClassLoader示例代码如下:

public class MyClassLoader extends ClassLoader{     private String rootDir; //class文件所在根目录     final private PermissionCollection permissionCollection; //本ClassLoader加载的类所在的ProtectionDomain拥有的权限集合     public MyClassLoader(String rootDir) {        this.rootDir = rootDir;        // 创建权限集合,各种权限类的使用可参考官网:https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html        this.permissionCollection = new MyPermissionCollection();        // 给tmp目录下的所有目录开启读、写权限        this.permissionCollection.add(new FilePermission("/tmp/-", "read,write"));        // 允许连接127.0.0.1:8080地址        this.permissionCollection.add(new SocketPermission("127.0.0.1:8080", "connect"));        // 权限添加完毕,需要将权限集合类设置为只读模式,防止被其他类修改        this.permissionCollection.setReadOnly();    }     // 建议重写findClass方法,如果重写loadClass方法,会破坏ClassLoaderd的双亲委派机制。    @Override    protected Class<?> findClass(String className) {        ByteArrayOutputStream baos = null;        BufferedInputStream bis = null;        try {            // class文件绝对路径            String fileName = rootDir + className + ".class";            // 读取class文件内容            bis = new BufferedInputStream(new FileInputStream(fileName));            baos = new ByteArrayOutputStream();            int len;            byte[] tmp = new byte[1024];            while((len = bis.read(tmp)) != -1) {                baos.write(tmp, 0, len);            }            byte[] classData = baos.toByteArray();            // 为该ClassLoader加载的类创建ProtectionDomain            ProtectionDomain pd = new ProtectionDomain(null, permissionCollection);            Class<?> clazz = defineClass(null, classData, 0, classData.length, pd);            return clazz;        } catch (IOException e) {            e.printStackTrace();        } finally {            if (baos != null) {                try {                    baos.close();                } catch (IOException e) {                    e.printStackTrace();                }            }            if (bis != null) {                try {                    bis.close();                } catch (IOException e) {                    e.printStackTrace();                }            }        }        return null;    } }

自定义PermissionCollection类示例代码如下:

public class MyPermissionCollection extends PermissionCollection {     private List<Permission> perms = new ArrayList<>();     @Override    public void add(Permission permission) {        if (isReadOnly())            throw new SecurityException(                    "attempt to add a Permission to a readonly PermissionCollection");         synchronized (this) {            perms.add(0, permission);        }    }     @Override    public boolean implies(Permission permission) {         synchronized (this) {            // 判断本次的操作对应的permission,是否已在perms集合中设置。            for (Permission x : perms) {                if (x.getClass() == permission.getClass() && x.implies(permission))                    return true;            }        }        return false;    }     @Override    public Enumeration<Permission> elements() {        synchronized (this) {            return Collections.enumeration(perms);        }    }}

测试代码如下:

/** * -Djava.security.manager -Djava.security.policy=/Users/hldf/CodeSpaces/java_workspaces/datastruct/src/main/resources/custom.policy */public class SecurityManagerDemo {     public static void main(String[] args) throws Exception {        new Thread(() -> {            try {                MyClassLoader myClassLoader = new MyClassLoader("/Users/hldf/Desktop/test/com/hldf/sandbox/");                Class<?> clazz = myClassLoader.loadClass("User");                clazz.newInstance();            } catch (Exception e) {                e.printStackTrace();            }        }, "t1").start();         try {            FileReader fr = new FileReader(new File("/tmp/xxx.txt"));            BufferedReader reader = new BufferedReader(fr);            String s;            while ((s = reader.readLine()) != null) {                System.out.println(s);            }        } catch (Exception e) {            System.out.println("main ...");            e.printStackTrace();        }        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }    }}

代码中都有详细的注释,这里就不赘述了。

总结

1、该方案通过为自定义ClassLoader导入的类设置专属沙箱,通过白名单方式为类添加权限,被绕过风险低,有效防御了任意代码执行问题。

2、由于Java沙箱机制为全局开启(官方仅支持全局开启,未找到仅对部分类库开启沙箱机制的方法),涉及的面广,生产环境使用时需谨慎评估。

PS:Java API中所有会调用SecurityManager进行权限校验的方法,大部分自身就比较耗时(如IO操作、反射调用、Socket相关API等)。从这个角度看,即使SecurityManager为全局开启,其也不会对系统整体性能产生很大的影响。

3、我们需要根据具体的业务场景,设置好权限集合(PermissionCollection),该权限集合必须遵循最小化开启原则,避免被绕过。

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。