《Java编码指南:编写安全可靠程序的75条建议》—— 指南18:不要将使用降低安全性检查的方法暴露给不可信代码...

本节书摘来异步社区《Java编码指南:编写安全可靠程序的75条建议》一书中的第1章,第1.18节,作者:【美】Fred Long(弗雷德•朗), Dhruv Mohindra(德鲁•莫欣达), Robert C.Seacord(罗伯特 C.西科德), Dean F.Sutherland(迪恩 F.萨瑟兰), David Svoboda(大卫•斯沃博达),更多章节内容可以访问云栖社区“异步社区”公众号查看。

指南18:不要将使用降低安全性检查的方法暴露给不可信代码

大多数方法缺乏安全管理器检查,是因为它们不提供对系统敏感部分(如文件系统)的访问。大多数提供安全管理器检查的方法,都是在调用堆栈中的每个类和方法被执行之前进行身份验证。这个安全模型允许Java applet这样的受限制程序对核心Java库具有完全访问权限。它还可以防止敏感方法扮演成藏身于可信的调用堆栈中的恶意方法。

但是,某些方法使用了降低安全性检查,只检查正在调用的方法是否已授权,而不检查调用堆栈中的每一个方法。任何调用这些方法的代码必须保证它们不能代表不可信的代码。表1-2列出了这些方法。

5c8046004f292d8779202e6e58ca61ac3b770419

因为方法java.lang.reflect.Field.setAccessible()和getAccessible()被用来通知Java虚拟机(JVM)进行覆盖语言访问检查,所以它们执行标准的(甚至更严格的)安全管理器检查,因此不会出现这条指南中描述的漏洞。然而,使用这些方法时也要倍加小心,其余set()和get()字段反射方法只执行语言访问检查,因此易受到攻击。

类加载器

类加载器允许Java应用程序通过加载额外的类而在运行时动态扩展。对于每个被加载的类,JVM都会跟踪用于加载该类的类加载器。当已加载的类第一次引用另一个类时,虚拟机请求使用该类的加载器来加载被引用的类。Java的类加载器架构通过使用不同的类加载器,来控制跟加载自不同来源的代码之间的交互。这种类加载器的分离是代码分离的基础:它可以防止恶意代码获取访问并破坏可信代码。

其中几个负责加载类的方法,将它们的工作委派给了被调用方法的类加载器。类加载器会执行与类的加载有关的安全检查。因此,任何调用其中的一个类加载方法的代码,必须保证这些方法不能代表不可信的代码。这些方法如表1-3所示。

e082defbec41547708b7a16f1bba7c75e453c784

除了loadLibrary()和load()方法,列表中其他方法不执行任何安全管理器检查;它们将安全检查委托给了适当的类加载器。

在实践中,可信代码的类加载器经常允许调用这些方法,而不可信代码的类加载器可能缺少这样的特权。然而,当不可信代码的类加载器委托给可信代码的类加载器时,可信代码对于不可信代码来说是可见的。在缺乏这样的委托关系时,类加载器会确保命名空间的分离,因此,不可信代码将无法观察属于可信代码的成员,也无法调用属于可信代码的方法。

类加载器委托模型是许多Java实现及框架的基础。要避免将表1-2和表1-3中列出的方法暴露给不可信的代码。例如,试想不可信代码试图加载一个特权类的攻击场景,如果它的类加载器自身缺少加载所请求的特权类的权限,但是,类加载器可以将类的加载委托给可信类的类加载器,那么就会发生特权升级。此外,如果可信代码接受被污染的输入,那么可信代码的类加载器就会代表不可信代码,加载恶意的特权类。

具有相同的类加载器定义的类,将会存在于相同的命名空间里,但根据安全策略的不同,它们可能具有不同的特权。当特权代码与同一个类加载器加载的无特权代码(或者更少特权的代码)共存时,就会出现安全漏洞。在这种情况下,更少特权的代码可以根据特权代码声明的可访问性,自由地访问特权代码的成员。使用上述表格中API的特权代码,能绕过安全管理器检查(loadLibrary()和load()方法除外)。

该指南类似于《The CERT® Oracle® Secure Coding Standard for Java™》[Long 2012]的“SEC03-J. Do not load trusted classes after allowing untrusted code to load arbitrary classes”。许多例子也违反“SEC00-J. Do not allow privileged blocks to leak sensitive information across a trust boundary”。

违规代码示例
下面的违规代码示例将System.loadLibrary()方法的调用嵌到了doPrivileged()语句块中。

public void load(String libName) {
 AccessController.doPrivileged(new PrivilegedAction() {
  public Object run() {
   System.loadLibrary(libName);
   return null;
  }
 });
}```
这段代码是不安全的,因为它可以代表不可信代码来加载一个库。在本质上,不可信代码的类加载器可以使用这段代码来加载一个库,即使它缺乏足够的权限直接去加载。加载一个库后,不可信代码可以从该库中调用可供访问的本地方法,因为doPrivileged()语句块会妨碍安全管理器检查被应用到调用者进而执行堆栈。

非本地库的代码也容易受相关安全漏洞的影响。假设存在一个库,该库包含一个没有直接暴露的漏洞,也许就藏在一个未被使用的方法中。加载这个库可能也不会直接暴露该漏洞。然而,攻击者可以加载一个额外的库,攻破第一个库的漏洞。此外,非本地库经常使用doPrivileged语句块,这让它们成了有吸引力的攻击目标。

合规解决方案
下面的合规解决方案对代码库的名称进行了硬编码,防止了输入值被污染的可能性。它同时也减少了load()方法的可访问性,从public(公共)变为private(私有)。因此,不可信的调用者被禁止加载awt库。

private void load() {
 AccessController.doPrivileged(new PrivilegedAction() {
  public Object run() {
   System.loadLibrary("awt");
   return null;
  }
 });
}`
违规代码示例
下面的违规代码示例将一个java.sql.Connection的实例从可信代码返回到不可信代码。

public Connection getConnection(String url, String username,
  String password) {
 // ...
 return DriverManager.getConnection(url, username, password);
}```
缺少创建SQL连接所需权限的不可信代码,可以通过使用直接获取的实例,绕过这些限制。getConnection()方法是不安全的,因为它使用url参数来指示要加载的类,这个类就是数据库驱动程序。

####合规解决方案
下面的合规解决方案可以防止恶意用户提供他们自己的数据库连接URL,从而限制了它们加载不可信的驱动程序。

private String url = // Hardwired value

public Connection getConnection(String username,
  String password) {
 // ...
 return DriverManager.getConnection(this.url,
  username, password);
}`
违规代码示例(CERT Vulnerability 636312)
CERT漏洞注解VU#636312描述了一个Java 1.7.0版本第6次更新中的漏洞,该漏洞在2012年8月被广泛利用。攻击程序实际上利用了两个漏洞,另一个的描述在《The CERT® Oracle® Secure Coding Standard for Java™》[Long 2012]的“SEC05-J. Do not use reflection to increase accessibility of classes, methods, or fields”里。

该攻击程序作为一个Java applet运行。applet的类加载器确保applet不能直接调用com.sun.*包中类的方法。一个正常的安全管理器检查,可以根据调用堆栈中所有调用者方法的特权(这些特权是和类的代码源相关联的),确定是允许还是拒绝特定动作。

攻击程序的第一个目标是访问私有的sun.awt.SunToolkit类。不过,用该类的名称直接调用class.forName()方法,将会导致抛出SecurityException异常。因此,攻击程序利用了下面的代码来访问任意类,绕过了安全管理器:

private Class GetClass(String paramString)
  throws Throwable {
 Object arrayOfObject[] = new Object[1];
 arrayOfObject[0] = paramString;
 Expression localExpression =
  new Expression(Class.class, "forName", arrayOfObject);
 localExpression.execute();
 return (Class)localExpression.getValue();
}```
java.beans.Expression.execute()方法将它的工作委托给了下面的方法:

private Object invokeInternal() throws Exception {
 Object target = getTarget();
 String methodName = getMethodName();
 if (target == null || methodName == null) {
  throw new NullPointerException(
   (target == null ? "target" : "methodName") +
    " should not be null");
 }
 Object[] arguments = getArguments();
 if (arguments == null) {
  arguments = emptyArray;
 }
 // Class.forName() won't load classes outside
 // of core from a class inside core, so it
 // is handled as a special case.
 if (target == Class.class && methodName.equals("forName")) {
  return ClassFinder.resolveClass((String)arguments[0],
               this.loader);
 }
// ...`
com.sun.beans.finder.ClassFinder.resolveClass()方法将它工作委托给了它的findClass()方法:

public static Class findClass(String name)
  throws ClassNotFoundException {
 try {
  ClassLoader loader =
   Thread.currentThread().getContextClassLoader();
  if (loader == null) {
   loader = ClassLoader.getSystemClassLoader();
  }
  if (loader != null) {
   return Class.forName(name, false, loader);
  }
 } catch (ClassNotFoundException exception) {
  // Use current class loader instead
 } catch (SecurityException exception) {
  // Use current class loader instead
 }
 return Class.forName(name);
}```
虽然这个方法是在applet的上下文中调用的,但它还是使用了class.forName()来获取所请求的类。Class.forName()将该搜索委托给调用方法的类加载器。在这种情况下,调用类(com.sun.beans.finder.ClassFinder)是Java核心的一部分,因此,可信的类加载器代替了受更多限制的applet类加载器,同时可信的类加载器加载了所请求的类,它并不知道自己正在为恶意代码服务。

####合规解决方案(CVE-2012-4681)
Oracle公司通过对com.sun.beans.finder.ClassFinder.findClass()方法打补丁,在Java 1.7.0版本的第7次更新中缓解了这个漏洞。在下面这个实例中,checkPackageAccess()方法检查整个调用堆栈,确保class.forName()只为可信代码获取类。

public static Class<?> findClass(String name)
  throws ClassNotFoundException {
 checkPackageAccess(name);
 try {
  ClassLoader loader =
   Thread.currentThread().getContextClassLoader();
  if (loader == null) {
   // Can be null in IE (see 6204697)
   loader = ClassLoader.getSystemClassLoader();
  }
  if (loader != null) {
   return Class.forName(name, false, loader);
  }

 } catch (ClassNotFoundException exception) {
  // Use current class loader instead
 } catch (SecurityException exception) {
  // Use current class loader instead
 }
 return Class.forName(name);
}`

违规代码示例(CVE-2013-0422)

Java 1.7.0版本的第10次更新在2013年1月因为几个漏洞被广泛攻击。其中有一个这样的漏洞:com.sun.jmx.mbeanserver.MBeanInstantiator类给无特权代码授予了访问任何类的权限,不受当前安全策略或可访问性规则的限制。可以通过任意一个字符串来调用MBeanInstantiator.findClass()方法,并尝试返回以该字符串命名的Class对象。这个方法将它的工作委派给了MBeanInstantiator.loadClass()方法,其源代码如下所示:

/**
* Load a class with the specified loader, or with this object
* class loader if the specified loader is null.
**/
static Class<?> loadClass(String className, ClassLoader loader)
  throws ReflectionException {
 Class<?> theClass;
 if (className == null) {
  throw new RuntimeOperationsException(
   new IllegalArgumentException(
    "The class name cannot be null"),
     "Exception occurred during object instantiation");
 } try {
  if (loader == null) {
   loader = MBeanInstantiator.class.getClassLoader();
  }
  if (loader != null) {
   theClass = Class.forName(className, false, loader);
  } else {
   theClass = Class.forName(className);
  }
 } catch (ClassNotFoundException e) {
  throw new ReflectionException(
   e, "The MBean class could not be loaded");
 }
 return theClass;
}```
这个方法将动态加载指定类的任务委托给了Class.forName()方法,Class.forName()又将其工作委托给了它调用的方法的类加载器。因为调用的方法是MBeanInstantiator.loadClass(),而它使用的是核心类加载器,因此没有提供安全检查。

####合规解决方案(CVE-2013-0422)
Oracle公司在Java 1.7.0版本的第11次更新中,添加了对MBeanInstantiator.loadClass()方法的访问检查,缓解了这个漏洞。这个访问检查确保了调用者可以访问所寻求的类。

// ...
 if (className == null) {
  throw new RuntimeOperationsException(
   new IllegalArgumentException(
    "The class name cannot be null"),
    "Exception occurred during object instantiation");
 }
 ReflectUtil.checkPackageAccess(className);
 try {
  if (loader == null)
// ...`

适用性

允许不可信代码调用降低安全性检查的方法,将会导致特权升级。同样地,允许不可信代码使用直接调用者的类加载器来执行操作,可能会允许不可信代码以与直接调用者相同的权限执行。

避免使用直接调用者的类加载器实例的方法,超出了本指南的讨论范围。例如,三参数的java.lang.Class.forName()方法需要一个显式的参数,用以指定要使用的类加载器实例。

public static Class forName(String name, boolean initialize,
    ClassLoader loader) throws ClassNotFoundException```
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值