为什么我自定义的SecurityManager在第16次使用Constructor.newInstance创建对象时会引发异常?

10

我目前正在开发一个小型Java应用程序,其中必须同时运行可信代码和不可信代码。为了实现这一点,我安装了一个自定义的 SecurityManager,在检查权限时会抛出 SecurityException

作为可信代码和不可信代码之间的桥梁,我有一个线程,使用 Constructor.newInstance() 来实例化一个不可信类型的对象。当它进行此调用时,安全管理器被配置为阻止一切。有趣的是,前15次使用 Constructor.newInstance() 创建对象时,一切正常,但第16次会出现 SecurityException

我已经将此简化为一个简单的测试程序:

import java.lang.reflect.*;
import java.security.*;

public class Main {
    /* Track how many instances have been created so that we can see when the exception
     * is thrown.
     */
    private static int numInstances = 0;
    public Main() {
        System.out.println("Number created: " + ++numInstances);
    }

    public static void main(String[] args) {
        /* Get the constructor for Main so that we can instantiate everything
         * later on.
         */
        Constructor<Main> ctor;
        try {
            ctor = Main.class.getConstructor();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
            return;
        }

        /* Install a super prohibitive security manager that disallows all operations. */
        System.setSecurityManager(new SecurityManager() {
            @Override
            public void checkPermission(Permission p) {
                /* Nothing is allowed - any permission check causes a     security
                 * exception.
                 */
                throw new SecurityException("Not permitted: " + p);
            }
        });

        /* Continuously create new Main objects. */
        try {
            while (true) {
                ctor.newInstance();
            }
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
    }
}

这个程序安装了一个SecurityManager,无论请求什么权限,其checkPermission都会抛出异常。然后程序进入循环并使用ctor.newInstance()来实例化一个无害的Main对象,并打印出到目前为止生成的实例数。在我的系统上,该程序的输出如下:

Number created: 1
Number created: 2
Number created: 3
Number created: 4
Number created: 5
Number created: 6
Number created: 7
Number created: 8
Number created: 9
Number created: 10
Number created: 11
Number created: 12
Number created: 13
Number created: 14
Number created: 15
java.lang.SecurityException: Not permitted: ("java.lang.RuntimePermission" "createClassLoader")
    at Main$1.checkPermission(Main.java:32)
    at java.lang.SecurityManager.checkCreateClassLoader(SecurityManager.java:611)
    at java.lang.ClassLoader.checkCreateClassLoader(ClassLoader.java:274)
    at java.lang.ClassLoader.<init>(ClassLoader.java:316)
    at sun.reflect.DelegatingClassLoader.<init>(ClassDefiner.java:72)
    at sun.reflect.ClassDefiner$1.run(ClassDefiner.java:60)
    at sun.reflect.ClassDefiner$1.run(ClassDefiner.java:58)
    at java.security.AccessController.doPrivileged(Native Method)
    at sun.reflect.ClassDefiner.defineClass(ClassDefiner.java:57)
    at sun.reflect.MethodAccessorGenerator$1.run(MethodAccessorGenerator.java:399)
    at sun.reflect.MethodAccessorGenerator$1.run(MethodAccessorGenerator.java:396)
    at java.security.AccessController.doPrivileged(Native Method)
    at sun.reflect.MethodAccessorGenerator.generate(MethodAccessorGenerator.java:395)
    at sun.reflect.MethodAccessorGenerator.generateConstructor(MethodAccessorGenerator.java:94)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:48)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
    at Main.main(Main.java:39)

根据RuntimePermissionJavadoc,授予createClassLoader权限是一项风险较高的操作:

授予此权限十分危险。恶意应用程序可以实例化其自己的类加载器并将其自己的恶意类加载到系统中。这些新加载的类可以由类加载器放置在任何保护域中,从而自动授予这些类该域的权限。

我有两个问题:

  1. 具体是什么原因导致了这个错误?为什么第16次循环时需要一个类加载器?我猜测这可能与Java尝试通过生成字节码来直接实例化对象以优化反射有关,但我不确定。

  2. 在不白名单列出createClassLoader特权(这是危险的)的情况下,是否有办法从受信任的代码中实例化不受信任的对象?

  3. 我是不是从根本上就走错了吗?

谢谢!


我尝试了你的代码,结果相同。如果你移除 throw exception 并添加一个 print statement,在创建第 16 个实例之前 checkPermission 会被调用 3 次。 - Sotirios Delimanolis
1个回答

12

请查看 GrepCode上的这个内容:

72  private static int     inflationThreshold = 15;

15 是膨胀阈值的默认值,在 NativeConstructorAccessorImpl 中引入更积极的优化之前,反射调用的计数为此值:

47 如果(++numInvocations > ReflectionFactory.inflationThreshold()){
48 ConstructorAccessorImpl acc = (ConstructorAccessorImpl)
49 新建 MethodAccessorGenerator().
50 generateConstructor(c.getDeclaringClass(),
51 c.getParameterTypes(),
52 c.getExceptionTypes(),
53 c.getModifiers());
54 parent.setDelegate(acc);

这段代码会导致一个新的类加载器被实例化,在第16次迭代时抛出异常。字节码生成发生在MethodAccessorGenerator类中,这是最有趣的部分:

387  // 载入类
388 vec.trim();
389 final byte[] bytes = vec.getData();
390 // 注意:唯一重要的是类加载器,将生成的代码放在与目标类相同的命名空间中。由于生成的代码本来就受特权保护,所以保护域可能并不重要。
391
392
393
394
395 return AccessController.doPrivileged(
396 new PrivilegedAction<MagicAccessorImpl>() {
397 public MagicAccessorImpl run() {
398 try {
399 return (MagicAccessorImpl)
400 ClassDefiner.defineClass
401 (generatedName,
402 bytes,
403 0,
404 bytes.length,
405 declaringClass.getClassLoader()).newInstance();
406 } catch (InstantiationException e) {
407 throw (InternalError)
408 new InternalError().initCause(e);
409 } catch (IllegalAccessException e) {
410 throw (InternalError)
411 new InternalError().initCause(e);
412 }
413 }
414 });
注:这段代码主要是动态加载类的实现,需要注意的是将生成的代码放入与目标类相同的命名空间中,以便正确地加载生成的类。同时,生成的代码受到特权保护,因此保护域可能并不重要。
关于授予权限,您仍有选择仔细为您的代码形成保护域并授予权限,而不授予外部代码。

我无法抗拒的诱惑 :) - Marko Topolnik
这太棒了。我仍在学习保护域,如果这是一个愚蠢的问题,我很抱歉,但我该如何构建域以防止不受信任的代码使用类加载器,同时让我的可信代码在newInstance调用时使用它?或者将整个内容放入保护域中,其中类加载器特权被撤销,会导致内部实现不尝试膨胀吗? - templatetypedef
不幸的是,我花了这段时间考虑,但我不知道如何授予JDK反射代码所需的权限,而不会在最终调用构造函数时传递授权。或许您可以完全禁用充气。 - Marko Topolnik
Java库在运行时随机需要权限。通常情况下,自1.2(1998年)以来,拥有“自定义”安全管理器并不值得。相反,检查会通过java.security.AccessController进行转发。查看示例代码,您的SecurityManager可能无法正常工作,因为它似乎不受信任。通过它传递的任何检查都将失败,因为它没有特权。毫不奇怪,管理信任的代码需要被信任(至少具有该信任级别的信任)。 - Tom Hawtin - tackline
@TomHawtin-tackline 如果通过“SecurityManager”,怎么会检查失败呢?它不是唯一有权使安全检查失败的机构吗? - Marko Topolnik
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接