зеркало из https://github.com/mozilla/rhino.git
Classcache must honor current security context. (#1019)
With this PR, the cache takes the current security context (in detail, the java.security.Permissions) into account when caching classes. Co-authored-by: Roland Praml <roland.praml@foconis.de> Co-authored-by: Roland Praml <pram@gmx.de>
This commit is contained in:
Родитель
9dc43c8f37
Коммит
cf58b9d8a7
|
@ -8,6 +8,7 @@ package org.mozilla.javascript;
|
|||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
|
@ -21,12 +22,42 @@ public class ClassCache implements Serializable {
|
|||
private static final long serialVersionUID = -8866246036237312215L;
|
||||
private static final Object AKEY = "ClassCache";
|
||||
private volatile boolean cachingIsEnabled = true;
|
||||
private transient Map<Class<?>, JavaMembers> classTable;
|
||||
private transient Map<CacheKey, JavaMembers> classTable;
|
||||
private transient Map<JavaAdapter.JavaAdapterSignature, Class<?>> classAdapterCache;
|
||||
private transient Map<Class<?>, Object> interfaceAdapterCache;
|
||||
private int generatedClassSerial;
|
||||
private Scriptable associatedScope;
|
||||
|
||||
/**
|
||||
* CacheKey is a combination of class and securityContext. This is required when classes are
|
||||
* loaded from different security contexts
|
||||
*/
|
||||
static class CacheKey {
|
||||
final Class<?> cls;
|
||||
final Object sec;
|
||||
/** Constructor. */
|
||||
public CacheKey(Class<?> cls, Object securityContext) {
|
||||
this.cls = cls;
|
||||
this.sec = securityContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = cls.hashCode();
|
||||
if (sec != null) {
|
||||
result = sec.hashCode() * 31;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return (obj instanceof CacheKey)
|
||||
&& Objects.equals(this.cls, ((CacheKey) obj).cls)
|
||||
&& Objects.equals(this.sec, ((CacheKey) obj).sec);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for ClassCache object in the given scope. The method first calls {@link
|
||||
* ScriptableObject#getTopLevelScope(Scriptable scope)} to get the top most scope and then tries
|
||||
|
@ -101,11 +132,11 @@ public class ClassCache implements Serializable {
|
|||
}
|
||||
|
||||
/** @return a map from classes to associated JavaMembers objects */
|
||||
Map<Class<?>, JavaMembers> getClassCacheMap() {
|
||||
Map<CacheKey, JavaMembers> getClassCacheMap() {
|
||||
if (classTable == null) {
|
||||
// Use 1 as concurrency level here and for other concurrent hash maps
|
||||
// as we don't expect high levels of sustained concurrent writes.
|
||||
classTable = new ConcurrentHashMap<Class<?>, JavaMembers>(16, 0.75f, 1);
|
||||
classTable = new ConcurrentHashMap<CacheKey, JavaMembers>(16, 0.75f, 1);
|
||||
}
|
||||
return classTable;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,9 @@ import java.lang.reflect.Field;
|
|||
import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.security.AccessControlContext;
|
||||
import java.security.AllPermission;
|
||||
import java.security.Permission;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
@ -28,6 +31,8 @@ import java.util.Map;
|
|||
* @see NativeJavaClass
|
||||
*/
|
||||
class JavaMembers {
|
||||
private static final Permission allPermission = new AllPermission();
|
||||
|
||||
JavaMembers(Scriptable scope, Class<?> cl) {
|
||||
this(scope, cl, false);
|
||||
}
|
||||
|
@ -749,16 +754,17 @@ class JavaMembers {
|
|||
Scriptable scope, Class<?> dynamicType, Class<?> staticType, boolean includeProtected) {
|
||||
JavaMembers members;
|
||||
ClassCache cache = ClassCache.get(scope);
|
||||
Map<Class<?>, JavaMembers> ct = cache.getClassCacheMap();
|
||||
Map<ClassCache.CacheKey, JavaMembers> ct = cache.getClassCacheMap();
|
||||
|
||||
Class<?> cl = dynamicType;
|
||||
Object secCtx = getSecurityContext();
|
||||
for (; ; ) {
|
||||
members = ct.get(cl);
|
||||
members = ct.get(new ClassCache.CacheKey(cl, secCtx));
|
||||
if (members != null) {
|
||||
if (cl != dynamicType) {
|
||||
// member lookup for the original class failed because of
|
||||
// missing privileges, cache the result so we don't try again
|
||||
ct.put(dynamicType, members);
|
||||
ct.put(new ClassCache.CacheKey(dynamicType, secCtx), members);
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
@ -789,16 +795,34 @@ class JavaMembers {
|
|||
}
|
||||
|
||||
if (cache.isCachingEnabled()) {
|
||||
ct.put(cl, members);
|
||||
ct.put(new ClassCache.CacheKey(cl, secCtx), members);
|
||||
if (cl != dynamicType) {
|
||||
// member lookup for the original class failed because of
|
||||
// missing privileges, cache the result so we don't try again
|
||||
ct.put(dynamicType, members);
|
||||
ct.put(new ClassCache.CacheKey(dynamicType, secCtx), members);
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
private static Object getSecurityContext() {
|
||||
Object sec = null;
|
||||
SecurityManager sm = System.getSecurityManager();
|
||||
if (sm != null) {
|
||||
sec = sm.getSecurityContext();
|
||||
if (sec instanceof AccessControlContext) {
|
||||
try {
|
||||
((AccessControlContext) sec).checkPermission(allPermission);
|
||||
// if we have allPermission, we do not need to store the
|
||||
// security object in the cache key
|
||||
return null;
|
||||
} catch (SecurityException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return sec;
|
||||
}
|
||||
|
||||
RuntimeException reportMemberNotFound(String memberName) {
|
||||
return Context.reportRuntimeErrorById(
|
||||
"msg.java.member.not.found", cl.getName(), memberName);
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.example.securitytest;
|
||||
|
||||
/**
|
||||
* Class for SecurityControllerTest.
|
||||
*
|
||||
* @author Roland Praml, FOCONIS AG
|
||||
*/
|
||||
public class SomeFactory {
|
||||
|
||||
public static int TEST = 42;
|
||||
|
||||
public SomeInterface create() {
|
||||
try {
|
||||
return (SomeInterface)
|
||||
Class.forName("com.example.securitytest.impl.SomeClass").newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
|
||||
throw new RuntimeException("Could not create impl", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.example.securitytest;
|
||||
|
||||
/**
|
||||
* Class for SecurityControllerTest.
|
||||
*
|
||||
* @author Roland Praml, FOCONIS AG
|
||||
*/
|
||||
public interface SomeInterface {
|
||||
|
||||
String foo();
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.example.securitytest.impl;
|
||||
|
||||
import com.example.securitytest.SomeInterface;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Provides an implementation for SomeInterface. Defines two methods: <code>foo</code> overridden
|
||||
* (defined by interface) and <code>bar</code> defined at this class.
|
||||
*
|
||||
* <p>If this class is excluded by the shutter, the method <code>bar</code> should not be accessible
|
||||
* in scripts.
|
||||
*
|
||||
* @author Roland Praml, FOCONIS AG
|
||||
*/
|
||||
public class SomeClass extends ArrayList<String> implements SomeInterface {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public String foo() {
|
||||
return "FOO";
|
||||
}
|
||||
|
||||
public String bar() {
|
||||
return "BAR";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
package org.mozilla.javascript.tests;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.security.CodeSource;
|
||||
import java.security.Permission;
|
||||
import java.security.Permissions;
|
||||
import java.security.Policy;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.security.URIParameter;
|
||||
import java.util.Enumeration;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mozilla.javascript.ClassShutter;
|
||||
import org.mozilla.javascript.EcmaError;
|
||||
import org.mozilla.javascript.Scriptable;
|
||||
import org.mozilla.javascript.SecurityController;
|
||||
import org.mozilla.javascript.tools.shell.Global;
|
||||
import org.mozilla.javascript.tools.shell.JavaPolicySecurity;
|
||||
|
||||
/** Perform some tests when we have a securityController in place. */
|
||||
public class SecurityControllerTest {
|
||||
|
||||
private static ProtectionDomain UNTRUSTED_JAVASCRIPT;
|
||||
private static ProtectionDomain ALLOW_IMPL_ACCESS;
|
||||
private static ProtectionDomain RESTRICT_IMPL_ACCESS;
|
||||
protected final Global global = new Global();
|
||||
|
||||
/** Sets up the security manager and loads the "grant-all-java.policy". */
|
||||
static void setupSecurityManager() {}
|
||||
/** Setup the security */
|
||||
@BeforeClass
|
||||
public static void setup() throws Exception {
|
||||
URL url = SecurityControllerTest.class.getResource("grant-all-java.policy");
|
||||
if (url != null) {
|
||||
System.setProperty("java.security.policy", url.toString());
|
||||
Policy.getPolicy().refresh();
|
||||
System.setSecurityManager(new SecurityManager());
|
||||
}
|
||||
SecurityController.initGlobal(new JavaPolicySecurity());
|
||||
|
||||
url = SecurityControllerTest.class.getResource("javascript.policy");
|
||||
Policy policy = Policy.getInstance("JavaPolicy", new URIParameter(url.toURI()));
|
||||
RESTRICT_IMPL_ACCESS = createProtectionDomain(policy, "RESTRICT_IMPL_ACCESS");
|
||||
ALLOW_IMPL_ACCESS = createProtectionDomain(policy, "ALLOW_IMPL_ACCESS");
|
||||
}
|
||||
|
||||
/** Creates a new protectionDomain with the given Code-Source Suffix. */
|
||||
private static ProtectionDomain createProtectionDomain(Policy policy, String csSuffix)
|
||||
throws MalformedURLException {
|
||||
File file = new File(System.getProperty("user.dir"));
|
||||
file = new File(file, "javascript");
|
||||
file = new File(file, csSuffix);
|
||||
URL url = file.toURI().toURL();
|
||||
CodeSource cs = new CodeSource(url, (java.security.cert.Certificate[]) null);
|
||||
Permissions perms = new Permissions();
|
||||
Enumeration<Permission> elems = policy.getPermissions(cs).elements();
|
||||
while (elems.hasMoreElements()) {
|
||||
perms.add(elems.nextElement());
|
||||
}
|
||||
perms.setReadOnly();
|
||||
return new ProtectionDomain(cs, perms, null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBarAccess() {
|
||||
// f.create produces "SomeClass extends ArrayList<String> implements
|
||||
// SomeInterface"
|
||||
// we may access array methods, like 'size' defined by ArrayList,
|
||||
// but not methods like 'bar' defined by SomeClass, because it is in a restricted package
|
||||
String script =
|
||||
"f = new com.example.securitytest.SomeFactory();\n"
|
||||
+ "var i = f.create();\n"
|
||||
+ "i.size();\n"
|
||||
+ "i.bar();";
|
||||
|
||||
// try in allowed scope
|
||||
runScript(script, ALLOW_IMPL_ACCESS);
|
||||
|
||||
try {
|
||||
// in restricted scope, we expect an EcmaError
|
||||
runScript(script, RESTRICT_IMPL_ACCESS);
|
||||
fail("EcmaError expected");
|
||||
} catch (EcmaError ee) {
|
||||
assertEquals("TypeError: Cannot find function bar in object []. (#4)", ee.getMessage());
|
||||
}
|
||||
|
||||
// try in allowed scope again
|
||||
runScript(script, ALLOW_IMPL_ACCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* This classShutter checks the "rhino.visible.{pkg}" runtime property, which can be defined in
|
||||
* a policy file. Note: Every other code in your stack-chain will need this permission also.
|
||||
*/
|
||||
private static class PolicyClassShutter implements ClassShutter {
|
||||
|
||||
@Override
|
||||
public boolean visibleToScripts(String fullClassName) {
|
||||
SecurityManager sm = System.getSecurityManager();
|
||||
if (sm != null) {
|
||||
int idx = fullClassName.lastIndexOf('.');
|
||||
if (idx != -1) {
|
||||
String pkg = fullClassName.substring(0, idx);
|
||||
sm.checkPermission(new RuntimePermission("rhino.visible." + pkg));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Compiles and runs the script with the given protection domain. */
|
||||
private void runScript(String scriptSourceText, ProtectionDomain pd) {
|
||||
Utils.runWithAllOptimizationLevels(
|
||||
context -> {
|
||||
context.setClassShutter(new PolicyClassShutter());
|
||||
Scriptable scope = context.initStandardObjects(global);
|
||||
|
||||
return context.evaluateString(scope, scriptSourceText, "", 1, pd);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
// Grant everyone the following permission: (required for SecurityControllerTest)
|
||||
grant {
|
||||
// permission all;
|
||||
permission java.security.AllPermission "", "";
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
// Sample script, how to define different security codebases for javaScript.
|
||||
|
||||
grant codebase "file:${user.dir}/javascript/ALLOW_IMPL_ACCESS" {
|
||||
permission java.lang.RuntimePermission "rhino.visible.com";
|
||||
permission java.lang.RuntimePermission "rhino.visible.com.example";
|
||||
permission java.lang.RuntimePermission "rhino.visible.com.example.securitytest";
|
||||
permission java.lang.RuntimePermission "rhino.visible.com.example.securitytest.*";
|
||||
};
|
||||
|
||||
grant codebase "file:${user.dir}/javascript/RESTRICT_IMPL_ACCESS" {
|
||||
permission java.lang.RuntimePermission "rhino.visible.com";
|
||||
permission java.lang.RuntimePermission "rhino.visible.com.example";
|
||||
permission java.lang.RuntimePermission "rhino.visible.com.example.securitytest";
|
||||
};
|
||||
|
||||
grant {
|
||||
// grant every script access to java.lang and java.util (but not to java.util.*)
|
||||
permission java.lang.RuntimePermission "rhino.visible.java";
|
||||
permission java.lang.RuntimePermission "rhino.visible.java.*";
|
||||
};
|
Загрузка…
Ссылка в новой задаче