在Java中,SecurityManager是否有一种方式可以有选择地授予ReflectPermission(“suppressAccessChecks”)?

8
有没有办法让 Java 中的 SecurityManager 根据 setAccessible() 调用的详细信息有选择地授予 ReflectPermission("suppressAccessChecks") 权限?我没有看到任何方法可以做到这一点。
对于一些沙箱代码,允许调用 setAccessible() 反射 API 将非常有用(例如运行各种动态 JVM 语言),但是只有在调用来自沙箱代码的类的方法/字段时才能调用 setAccessible()。
如果这不可能实现,有人有什么选择性授予 ReflectPermission("suppressAccessChecks") 以外的替代建议吗?也许在 SecurityManager.checkMemberAccess() 足够严格的情况下,在所有情况下都授予权限会更安全?
3个回答

12

也许查看调用堆栈就足够满足你的需求了?像这样:

import java.lang.reflect.ReflectPermission;
import java.security.Permission;

public class Test {
    private static int foo;

    public static void main(String[] args) throws Exception {
        System.setSecurityManager(new SecurityManager() {
            @Override
            public void checkPermission(Permission perm) {
                if (perm instanceof ReflectPermission && "suppressAccessChecks".equals(perm.getName())) {
                    for (StackTraceElement elem : Thread.currentThread().getStackTrace()) {
                        if ("Test".equals(elem.getClassName()) && "badSetAccessible".equals(elem.getMethodName())) {
                            throw new SecurityException();
                        }
                    }
                }
            }
        });

        goodSetAccessible(); // works
        badSetAccessible(); // throws SecurityException
    }

    private static void goodSetAccessible() throws Exception {
        Test.class.getDeclaredField("foo").setAccessible(true);
    }

    private static void badSetAccessible() throws Exception {
        Test.class.getDeclaredField("foo").setAccessible(true);
    }
}

3
这可以通过使用像Byte Buddy这样的库进行字节码编织来实现。不使用标准的ReflectPermission("suppressAccessChecks")权限,而是创建一个自定义权限,并使用Byte Buddy转换替换AccessibleObject.setAccessible方法,检查您的自定义权限的自定义方法。
此自定义权限可能的一种工作方式是基于调用者的类加载器和正在修改访问的对象。使用这种方法允许隔离的代码(从具有自己的类加载器的分离代码中加载)在其自己的jar中调用setAccessible,但不能调用标准Java类或您自己的应用程序类。
这样的权限可能如下所示:
public class UserSetAccessiblePermission extends Permission {
  private final ClassLoader loader;

  public UserSetAccessiblePermission(ClassLoader loader) {
    super("userSetAccessible");
    this.loader = loader;
  }  

  @Override
  public boolean implies(Permission permission) {
    if (!(permission instanceof UserSetAccessiblePermission)) {
      return false;
    }
    UserSetAccessiblePermission that = (UserSetAccessiblePermission) permission;
    return that.loader == this.loader;
  }

  // equals and hashCode omitted  

  @Override
  public String getActions() {
    return "";
  }
}

这是我选择实现该权限的方式,但它也可以是软件包或类白名单或黑名单。
现在有了这个权限,你可以创建一个存根类来替换AccessibleObject.setAcessible方法,并改为使用此权限。
public class AccessibleObjectStub {
  private final static Permission STANDARD_ACCESS_PERMISSION =
      new ReflectPermission("suppressAccessChecks");

  public static void setAccessible(@This AccessibleObject ao, boolean flag)
      throws SecurityException {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      Permission permission = STANDARD_ACCESS_PERMISSION;
      if (isFromUserLoader(ao)) {
        try {
          permission = getUserAccessPermission(ao);
        } catch (Exception e) {
          // Ignore. Use standard permission.
        }
      }

      sm.checkPermission(permission);
    }
  }

  private static Permission getUserAccessPermission(AccessibleObject ao)
      throws IllegalAccessException, InvocationTargetException, InstantiationException,
      NoSuchMethodException, ClassNotFoundException {
    ClassLoader aoClassLoader = getAccessibleObjectLoader(ao);
    return new UserSetAccessiblePermission(aoClassLoader);
  }

  private static ClassLoader getAccessibleObjectLoader(AccessibleObject ao) {
    return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
      @Override
      public ClassLoader run() {
        if (ao instanceof Executable) {
          return ((Executable) ao).getDeclaringClass().getClassLoader();
        } else if (ao instanceof Field) {
          return ((Field) ao).getDeclaringClass().getClassLoader();
        }
        throw new IllegalStateException("Unknown AccessibleObject type: " + ao.getClass());
      }
    });
  }

  private static boolean isFromUserLoader(AccessibleObject ao) {
    ClassLoader loader = getAccessibleObjectLoader(ao);

    if (loader == null) {
      return false;
    }

    // Check that the class loader instance is of a custom type
    return UserClassLoaders.isUserClassLoader(loader);
  }
}

使用这两个类,您现在可以使用Byte Buddy构建一个转换器来转换Java AccessibleObject以使用您的存根。
创建转换器的第一步是创建一个Byte Buddy类型池,其中包括引导类和包含您的存根的jar文件。
final TypePool bootstrapTypePool = TypePool.Default.of(
new ClassFileLocator.Compound(
    new ClassFileLocator.ForJarFile(jarFile),
    ClassFileLocator.ForClassLoader.of(null)));

接下来使用反射获取对AccessObject.setAccessible0方法的引用。这是一个私有方法,如果调用setAccessible时通过了权限检查,它实际上会修改可访问性。
Method setAccessible0Method;
try {
  String setAccessible0MethodName = "setAccessible0";
  Class[] paramTypes = new Class[2];
  paramTypes[0] = AccessibleObject.class;
  paramTypes[1] = boolean.class;
  setAccessible0Method = AccessibleObject.class
      .getDeclaredMethod(setAccessible0MethodName, paramTypes);
} catch (NoSuchMethodException e) {
  throw new RuntimeException(e);
}

使用这两个零件可以制造变压器。
AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
  @Override
  public DynamicType.Builder<?> transform(
      DynamicType.Builder<?> builder,
      TypeDescription typeDescription, ClassLoader classLoader) {
    return builder.method(
        ElementMatchers.named("setAccessible")
            .and(ElementMatchers.takesArguments(boolean.class)))
        .intercept(MethodDelegation.to(
            bootstrapTypePool.describe(
                "com.leacox.sandbox.security.stub.java.lang.reflect.AccessibleObjectStub")
                .resolve())
            .andThen(MethodCall.invoke(setAccessible0Method).withThis().withAllArguments()));
  }
}

最后一步是安装Byte Buddy Java代理并执行转换。包含存根的jar文件必须附加到引导类路径中。这是必要的,因为AccessibleObject类将由引导加载器加载,因此任何存根也必须在那里加载。
Instrumentation instrumentation = ByteBuddyAgent.install();
// Append the jar containing the stub replacement to the bootstrap classpath
instrumentation.appendToBootstrapClassLoaderSearch(jarFile);

AgentBuilder agentBuilder = new AgentBuilder.Default()
       .disableClassFormatChanges()
       .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
       .ignore(none()); // disable default ignores so we can transform Java classes
       .type(ElementMatchers.named("java.lang.reflect.AccessibleObject"))
       .transform(transformer)
       .installOnByteBuddyAgent();

当使用SecurityManager并将存根类和应用选择性权限的代码隔离在运行时加载的单独的jar中时,此方法可行。必须在运行时加载jar而不是作为标准依赖项或捆绑库,这使得事情变得有些复杂,但这似乎是使用SecurityManager隔离不受信任代码的要求。
我的Github存储库sandbox-runtime具有完整且深入的示例,其中包括执行隔离的不受信任代码以及更多选择性反射权限。我还有一篇博客文章详细介绍了selective setAccessible permissions部分。

0
FWI:由于setAccessible似乎只在序列化时有有效用例,因此我认为您可能经常可以直接拒绝它。
话虽如此,我对一般情况下如何做到这一点很感兴趣,因为我也必须编写安全管理器来阻止动态加载的代码执行我们的应用程序容器代码需要能够执行的操作。

不幸的是,一些动态JVM语言很喜欢使用setAccessible,即使对于他们不需要调用的公共方法也会这样做。此外,还有像你提到的序列化或某些依赖注入框架的操作模式等使用情况,最好不要无端阻塞。 - Alex Schultz
嗯,我不知道那些其他用例 - 我一直认为setAccessible是Sun在Java中犯的最大安全漏洞。 - Lawrence Dol

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