Java沙箱SecurityManager的妙用
业务系统提供了一个插件功能,用户可以根据插件规范来编写插件,然后将其制作为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),该权限集合必须遵循最小化开启原则,避免被绕过。
免责声明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。