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:
Roland Praml 2021-09-29 08:39:22 +02:00 коммит произвёл GitHub
Родитель 9dc43c8f37
Коммит cf58b9d8a7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 292 добавлений и 8 удалений

Просмотреть файл

@ -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.*";
};