今天来看下类加载过程中的一些安全检查。在那之前得先来了解下Java的Access Control。
Access Control
The access control architecture in the Java platform protects access to sensitive resources (for example, local files) or sensitive application code (for example, methods in a class). All access control decisions are mediated by a security manager, represented by the
java.lang.SecurityManager
class. ASecurityManager
must be installed into the Java runtime in order to activate the access control checks.
访问控制属于Java Security Architecture的一部分。一张图看懂:)
alright,接下来我们还是直接看代码吧。FileInputStream
,
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
/////// 进行安全检查
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.incrementAndGetUseCount();
this.path = name;
open(name);
}
SecurityManager#checkRead(java.lang.String)
,
public void checkRead(String file) {
checkPermission(new FilePermission(file,
SecurityConstants.FILE_READ_ACTION));
}
public void checkPermission(Permission perm) {
/////// 直接交由AccessController处理
java.security.AccessController.checkPermission(perm);
}
AccessController#checkPermission
,
public static void checkPermission(Permission perm)
throws AccessControlException
{
//System.err.println("checkPermission "+perm);
//Thread.currentThread().dumpStack();
if (perm == null) {
throw new NullPointerException("permission can‘t be null");
}
AccessControlContext stack = getStackAccessControlContext();
// context为null不需要进行检查
// if context is null, we had privileged system code on the stack.
if (stack == null) {
Debug debug = AccessControlContext.getDebug();
boolean dumpDebug = false;
if (debug != null) {
dumpDebug = !Debug.isOn("codebase=");
dumpDebug &= !Debug.isOn("permission=") ||
Debug.isOn("permission=" + perm.getClass().getCanonicalName());
}
if (dumpDebug && Debug.isOn("stack")) {
Thread.currentThread().dumpStack();
}
if (dumpDebug && Debug.isOn("domain")) {
debug.println("domain (context is null)");
}
if (dumpDebug) {
debug.println("access allowed "+perm);
}
return;
}
/////// 交由AccessControlContext处理
AccessControlContext acc = stack.optimize();
acc.checkPermission(perm);
}
AccessControlContext#checkPermission
,
/* if ctxt is null, all we had on the stack were system domains,
or the first domain was a Privileged system domain. This
is to make the common case for system code very fast */
/////// 与classloader为null同一招
if (context == null)
return;
for (int i=0; i< context.length; i++) {
if (context[i] != null && !context[i].implies(perm)) {
if (dumpDebug) {
debug.println("access denied " + perm);
}
if (Debug.isOn("failure") && debug != null) {
// Want to make sure this is always displayed for failure,
// but do not want to display again if already displayed
// above.
if (!dumpDebug) {
debug.println("access denied " + perm);
}
Thread.currentThread().dumpStack();
final ProtectionDomain pd = context[i];
final Debug db = debug;
AccessController.doPrivileged (new PrivilegedAction<Void>() {
public Void run() {
db.println("domain that failed "+pd);
return null;
}
});
}
throw new AccessControlException("access denied "+perm, perm);
}
}
上面的context
是一个ProtectionDomain
数组,
private ProtectionDomain context[];
所以接下来是交由ProtectionDomain#implies
进行检查了,
public boolean implies(Permission permission) {
if (hasAllPerm) {
// internal permission collection already has AllPermission -
// no need to go to policy
return true;
}
if (!staticPermissions &&
Policy.getPolicyNoCheck().implies(this, permission))
return true;
if (permissions != null)
return permissions.implies(permission);
return false;
}
到这里终于出现Policy
了,Policy#getPolicyNoCheck
,
static Policy getPolicyNoCheck()
{
PolicyInfo pi = policy.get();
// Use double-check idiom to avoid locking if system-wide policy is
// already initialized
if (pi.initialized == false || pi.policy == null) {
synchronized (Policy.class) {
PolicyInfo pinfo = policy.get();
if (pinfo.policy == null) {
String policy_class = AccessController.doPrivileged(
new PrivilegedAction<String>() {
public String run() {
/////// 读取{java.home}/lib/security/java.security配置文件
/////// policy.provider=sun.security.provider.PolicyFile
return Security.getProperty("policy.provider");
}
});
if (policy_class == null) {
policy_class = "sun.security.provider.PolicyFile";
}
try {
pinfo = new PolicyInfo(
(Policy) Class.forName(policy_class).newInstance(),
true);
} catch (Exception e) {
/*
* The policy_class seems to be an extension
* so we have to bootstrap loading it via a policy
* provider that is on the bootclasspath.
* If it loads then shift gears to using the configured
* provider.
*/
// install the bootstrap provider to avoid recursion
Policy polFile = new sun.security.provider.PolicyFile();
pinfo = new PolicyInfo(polFile, false);
policy.set(pinfo);
final String pc = policy_class;
Policy pol = AccessController.doPrivileged(
new PrivilegedAction<Policy>() {
public Policy run() {
try {
ClassLoader cl =
ClassLoader.getSystemClassLoader();
// we want the extension loader
ClassLoader extcl = null;
while (cl != null) {
extcl = cl;
cl = cl.getParent();
}
return (extcl != null ? (Policy)Class.forName(
pc, true, extcl).newInstance() : null);
} catch (Exception e) {
if (debug != null) {
debug.println("policy provider " +
pc +
" not available");
e.printStackTrace();
}
return null;
}
}
});
/*
* if it loaded install it as the policy provider. Otherwise
* continue to use the system default implementation
*/
if (pol != null) {
pinfo = new PolicyInfo(pol, true);
} else {
if (debug != null) {
debug.println("using sun.security.provider.PolicyFile");
}
pinfo = new PolicyInfo(polFile, true);
}
}
policy.set(pinfo);
}
return pinfo.policy;
}
}
return pi.policy;
}
public boolean implies(ProtectionDomain domain, Permission permission) {
PermissionCollection pc;
if (pdMapping == null) {
initPolicy(this);
}
synchronized (pdMapping) {
pc = pdMapping.get(domain.key);
}
if (pc != null) {
return pc.implies(permission);
}
pc = getPermissions(domain);
if (pc == null) {
return false;
}
synchronized (pdMapping) {
// cache it
pdMapping.put(domain.key, pc);
}
return pc.implies(permission);
}
所以最终其实都是交由PermissionCollection#implies
处理,
/**
* Checks to see if the specified permission is implied by
* the collection of Permission objects held in this PermissionCollection.
*
* @param permission the Permission object to compare.
*
* @return true if "permission" is implied by the permissions in
* the collection, false if not.
*/
public abstract boolean implies(Permission permission);
总结一下就是,
- 通过
System.getSecurityManager()
拿到SecurityManager
; SecurityManager
直接交给AccessController
处理;AccessController
通过调用getStackAccessControlContext
取得AccessControlContext
,并交给AccessControlContext
处理;AccessControlContext
交给它所持有的一个ProtectionDomain
数组处理;ProtectionDomain
交给PermissionCollection
处理,有两种方式拿到PermissionCollection
,一种是使用自身持有的PermissionCollection
(构造函数传入),另一种是使用Policy
来获得。使用哪种方式由staticPermissions
决定。
那么现在有两个问题,System.getSecurityManager()
跟getStackAccessControlContext
分别做了啥?我们一个一个来看下。
SecurityManager
System.getSecurityManager()
其实比较简单,直接返回了一个SecurityManager
,
public static SecurityManager getSecurityManager() {
return security;
}
那么这个security
又是什么时候设置的呢?是在虚拟机启动的时候,由launcher来设置的,
public Launcher() {
...
// Finally, install a security manager if requested
String s = System.getProperty("java.security.manager");
if (s != null) {
SecurityManager sm = null;
if ("".equals(s) || "default".equals(s)) {
sm = new java.lang.SecurityManager();
} else {
try {
sm = (SecurityManager)loader.loadClass(s).newInstance();
} catch (IllegalAccessException e) {
} catch (InstantiationException e) {
} catch (ClassNotFoundException e) {
} catch (ClassCastException e) {
}
}
if (sm != null) {
System.setSecurityManager(sm);
} else {
throw new InternalError(
"Could not create SecurityManager: " + s);
}
}
}
所以我们是可以通过设置java.security.manager
这个系统属性来使用我们自己的SecurityManager
的。
getStackAccessControlContext
接下来看看比较关键的getStackAccessControlContext
方法,
/**
* Returns the AccessControl context. i.e., it gets
* the protection domains of all the callers on the stack,
* starting at the first class with a non-null
* ProtectionDomain.
*
* @return the access control context based on the current stack or
* null if there was only privileged system code.
*/
private static native AccessControlContext getStackAccessControlContext();
是个本地方法,具体实现我们暂不深究,下面直接贴出这个地方所使用的权限校验算法,具体参考官方文档,
Suppose the current thread traversed m callers, in the order of caller 1 to caller 2 to caller m. Then caller m invoked the checkPermission method. The basic algorithm checkPermission uses to determine whether access is granted or denied is the following
i = m;
while (i > 0) {
if (caller i‘s domain does not have the permission)
throw AccessControlException
else if (caller i is marked as privileged) {
if (a context was specified in the call to doPrivileged)
context.checkPermission(permission);
return;
}
i = i - 1;
};
// Next, check the context inherited when
// the thread was created. Whenever a new thread is created, the
// AccessControlContext at that time is
// stored and associated with the new thread, as the "inherited"
// context.
inheritedContext.checkPermission(permission);
有一点值得说明下,就是我们平时经常看到的AccessController#doPrivileged
方法,
That is, a caller can be marked as being “privileged” when it calls the
doPrivileged
method. When making access control decisions, thecheckPermission
method stops checking if it reaches a caller that was marked as “privileged” via adoPrivileged
call without a context argument. If that caller’s domain has the specified permission, no further checking is done andcheckPermission
returns quietly, indicating that the requested access is allowed. If that domain does not have the specified permission, an exception is thrown, as usual.
也就是说遇到privileged的caller,安全检查一定会停止,不管是成功还是失败。应该是出于性能考虑。
Protection Domain
AccessControlContext
我们没有深究到底是怎么生成的,那它所持有的ProtectionDomain
这货又是个啥?
A domain conceptually encloses a set of classes whose instances are granted the same set of permissions. Protection domains are determined by the policy currently in effect.
还是一张图看懂:)
每个domain所拥有的权限就是我们上面所看到的PermissionCollection
。
AccessControlContext
所持有的ProtectionDomain
其实就是在调用栈上面,每个Class
所属的ProtectionDomain
。那么每个Class
所属的ProtectionDomain
又是怎么来的呢?妥妥的,就是在类加载的时候塞进去的,这个我们下面会再分析。
再放一个栗子,来看看ProtectionDomain
具体长啥样,
public static void main(String[] args) throws Throwable {
System.out.println(System.class.getProtectionDomain());
System.out.println(GroovyClassLoader.class.getProtectionDomain());
System.out.println(Main.class.getProtectionDomain());
}
ProtectionDomain null
null
<no principals>
java.security.Permissions@73c94b51 (
("java.security.AllPermission" "<all permissions>" "<all actions>")
)
ProtectionDomain (file:/C:/Java/jdk1.7.0_51/jre/lib/ext/groovy-2.4.0.jar <no signer certificates>)
sun.misc.Launcher$ExtClassLoader@d325aef
<no principals>
java.security.Permissions@3aeb3f66 (
("java.io.FilePermission" "\C:\Java\jdk1.7.0_51\jre\lib\ext\groovy-2.4.0.jar" "read")
)
ProtectionDomain (file:/E:/Projects/just4fun/target/classes/ <no signer certificates>)
sun.misc.Launcher$AppClassLoader@35f784d7
<no principals>
java.security.Permissions@2a8f5fc2 (
("java.lang.RuntimePermission" "exitVM")
("java.io.FilePermission" "\E:\Projects\just4fun\target\classes\-" "read")
)
OK,接下来就来看下类加载过程中的一些安全检查。
findClass
首先是findClass
过程中,寻找Resource
时会有安全检查,代码在URLClassPath#check
,
/*
* Check whether the resource URL should be returned.
* Throw exception on failure.
* Called internally within this file.
*/
static void check(URL url) throws IOException {
SecurityManager security = System.getSecurityManager();
if (security != null) {
URLConnection urlConnection = url.openConnection();
Permission perm = urlConnection.getPermission();
if (perm != null) {
try {
security.checkPermission(perm);
} catch (SecurityException se) {
// fallback to checkRead/checkConnect for pre 1.2
// security managers
if ((perm instanceof java.io.FilePermission) &&
perm.getActions().indexOf("read") != -1) {
security.checkRead(perm.getName());
} else if ((perm instanceof
java.net.SocketPermission) &&
perm.getActions().indexOf("connect") != -1) {
URL locUrl = url;
if (urlConnection instanceof JarURLConnection) {
locUrl = ((JarURLConnection)urlConnection).getJarFileURL();
}
security.checkConnect(locUrl.getHost(),
locUrl.getPort());
} else {
throw se;
}
}
}
}
}
如果是本地资源需要校验文件权限,如果是网络资源需要校验网络权限。
getAndVerifyPackage
接下来在defineClass
的时候,首先需要一些关于package的检查,URLClassLoader#getAndVerifyPackage
,
private Package getAndVerifyPackage(String pkgname,
Manifest man, URL url) {
Package pkg = getPackage(pkgname);
if (pkg != null) {
// Package found, so check package sealing.
if (pkg.isSealed()) {
// Verify that code source URL is the same.
if (!pkg.isSealed(url)) {
throw new SecurityException(
"sealing violation: package " + pkgname + " is sealed");
}
} else {
// Make sure we are not attempting to seal the package
// at this code source URL.
if ((man != null) && isSealed(pkgname, man)) {
throw new SecurityException(
"sealing violation: can‘t seal package " + pkgname +
": already loaded");
}
}
}
return pkg;
}
类加载的时候会将类的package定义保存下来,其中包括package的一些相关属性,这里需要校验的主要是看看package的Sealed
属性。Sealed
属性通过Manifest
来定义,这其实是一个关于Java中package访问权限的补充。看下面的栗子,
package me.kisimple.just4fun;
public class Bar {
static String secret = "you know too much";
}
package me.kisimple.just4fun;
public class Foo {
public static String gotIt() {
return Bar.secret;
}
}
Bar#secret
是package的访问权限,Foo
可以访问到它,这是没有问题的。但假如我不希望Bar#secret
被其他jar包访问到,不怀好意的人却可以通过将Foo
打进自己jar包的方法来访问secret
,就像下面这样,
> jar cvf bar.jar me/kisimple/just4fun/Bar.class
> jar cvf foo.jar me/kisimple/just4fun/Foo.class
然后我同时依赖bar.jar
与foo.jar
,这样我就可以在自己代码里面通过Foo
间接访问Bar#secret
了,
public static void main(String[] args) throws Throwable {
System.out.println(Foo.gotIt());
}
这不得不说是Java的package访问权限的一个漏洞,怎么办呢?这时候就可以使用上面我们说到的Manifest
的Sealed
属性了,在打Bar
的jar包的时候,我们可以指定Manifest
并添加Sealed
属性,就像下面这样,
Name: me/kisimple/just4fun/
Sealed: true
> jar cvfm bar.jar imanifest me/kisimple/just4fun/Bar.class
OK,这时候再通过Foo
来访问Bar#secret
的时候将会报错,Foo
是无法被类加载器加载的,
Exception in thread "main" java.lang.SecurityException: sealing violation: can‘t seal package me.kisimple.just4fun: already loaded
at java.net.URLClassLoader.getAndVerifyPackage(URLClassLoader.java:395)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:417)
at java.net.URLClassLoader.access$100(URLClassLoader.java:71)
at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
at me.kisimple.just4fun.Foo.gotIt(Foo.java:10)
at me.kisimple.just4fun.Main.main(Main.java:10)
报错的地方其实就在getAndVerifyPackage
方法,具体可以再回过头去看看代码。
getPermissions
接下来在构建Class
所属的ProtectionDomain
时,也有安全检查,URLClassLoader#getPermissions
// make sure the person that created this class loader
// would have this permission
if (p != null) {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
final Permission fp = p;
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() throws SecurityException {
sm.checkPermission(fp);
return null;
}
}, acc);
}
perms.add(p);
}
需要确保这个即将grant给该ProtectionDomain
的权限,类加载器自己是有的。
preDefineClass
接下来是真正defineClass
之前最后的检查了,ClassLoader#preDefineClass
,
/* Determine protection domain, and check that:
- not define java.* class,
- signer of this class matches signers for the rest of the classes in
package.
*/
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf(‘.‘)));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
注释写得很清楚,有两个校验,确保不是在加载java.*
包下的类,确保相同package的签名信息是一致的(类似上面的package访问权限的校验)。
getProtectionDomain
alright,到这里安全检查的话题就结束了。但是上面我们还留了一个坑,就是ProtectionDomain
在类加载的时候是怎么整出来的。
我们来看看几个类加载器defineClass
方法签名,
/////// java.net.URLClassLoader
defineClass(String name, Resource res)
/////// java.security.SecureClassLoader
defineClass(String name, ByteBuffer b, CodeSource cs)
/////// java.lang.ClassLoader
defineClass(String name, ByteBuffer b, ProtectionDomain pd)
其实就是将Resource
转化成了ByteBuffer
和CodeSource
,然后再将CodeSource
转化成了ProtectionDomain
。来看下具体的SecureClassLoader#getProtectionDomain
,
private ProtectionDomain getProtectionDomain(CodeSource cs) {
if (cs == null)
return null;
ProtectionDomain pd = null;
synchronized (pdcache) {
pd = pdcache.get(cs);
if (pd == null) {
PermissionCollection perms = getPermissions(cs);
pd = new ProtectionDomain(cs, perms, this, null);
pdcache.put(cs, pd);
if (debug != null) {
debug.println(" getPermissions "+ pd);
debug.println("");
}
}
}
return pd;
}
getPermissions
方法由子类自己来定义,也就是要定义这个Class
所属的ProtectionDomain
所拥有的权限了。具体可以再去看看Launcher.AppClassLoader
和URLClassLoader
各自的实现,这样也能更好的理解上面我们栗子中所输出的ProtectionDomain
的值。
参考资料
- http://docs.oracle.com/javase/8/docs/technotes/guides/security/spec/security-specTOC.fm.html
- https://docs.oracle.com/javase/tutorial/deployment/jar/sealman.html