Java Security Manager: ограничения на код из внешнего jar, загруженного через ServiceLoader
Чего я пытаюсь достичь? Я работаю над Java-приложением, которое может быть расширено дополнительными банками, которые интегрируются через ServiceLoader. Эти загруженные расширения должны запускаться с некоторыми ограничениями SecurityManager, разумеется, просто для повышения безопасности. В качестве примера каждое Расширение должно получить один конкретный каталог, в котором оно может хранить что угодно, но доступ к любому другому файлу / папке должен быть ограничен. Основным приложением является доверенный код, и поэтому он может работать без каких-либо ограничений. Кроме того, основное приложение предоставляет некоторые реализации API для каждого расширения, которые также должны работать без ограничений. Это означает, что расширение не должно обращаться к файлу за пределами своего каталога, но когда расширение вызывает метод API, который пытается получить доступ к любому другому файлу, доступ должен быть предоставлен.
Вопрос Как я могу добиться упомянутого поведения, когда ограничиваются только "прямые" вызовы из классов расширений, но нет кода из основного приложения? Запуск расширений в разных потоках / threadGroups может быть хорошим решением в любом случае, но, поскольку вызовы API могут выполняться в одном потоке (группе), это может не помочь определить, должен ли доступ быть ограничен или не основан только на потоке.
Пример Я создал упрощенную тестовую среду. С одной стороны, есть эти два интерфейса:
public interface Extension {
void doSomethingRestricted();
void doSameViaApi(ExtensionApi api);
}
public interface ExtensionApi {
void doSomethingWithHigherPermissions();
}
Для тестирования я создал банку, содержащую это расширение:
public class SomeExtension implements Extension {
public void doSomethingRestricted() {
System.out.println(System.getProperty("user.home"));
}
public void doSameViaApi(final ExtensionApi api) {
api.doSomethingWithHigherPermissions();
}
}
В основном приложении я хотел бы сделать что-то вроде этого:
final ExtensionApi api = () -> System.out.println(System.getProperty("user.home"));
try {
final URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarFile.toURI().toURL() });
for(final Extension extension : ServiceLoader.load(Extension.class, urlClassLoader)) {
extension.doSomethingRestricted();
extension.doSameViaApi(api);
}
}
Поэтому, когда я звоню extension.doSomethingRestricted();
это должно привести к SecurityException, но вызов extension.doSameViaApi(api);
должно работать просто отлично. Таким образом, оба метода пытаются сделать то же самое, но один пытается сделать это через вызов API. Единственный подход, о котором я мог подумать, - это перебирать историю вызовов и проверять загрузчики классов, чтобы проанализировать, основан ли запрос на доступе на доверенном коде или коде расширения. Но я чувствую, что это может быть неприятное, подверженное ошибкам решение, так что, может быть, я пропустил несколько лучших подходов?
1 ответ
Во-первых, убедитесь, что ваши "основные" классы JAR пользуются всеми привилегиями. Программно это может быть выполнено следующим образом:
package q46991566;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Policy;
import java.util.Collections;
public class Main {
public static void main(String... args) throws Exception {
// policy configuration contents: this JAR gets all permissions, others get nothing
StringBuilder sb = new StringBuilder("grant {};\n\ngrant codebase \"")
.append(Main.class.getProtectionDomain().getCodeSource().getLocation())
.append("\" {\n\tpermission java.security.AllPermission;\n};\n");
// temp-save the policy configuration
Path policyPath = Files.createTempFile(null, null);
Files.write(policyPath, Collections.singleton(sb.toString()));
// convey to the default file-backed policy provider where to obtain its configuration from;
// leading equals ensures only the specified config file gets processed
System.setProperty("java.security.policy", "=".concat(policyPath.toUri().toURL().toString()));
// establish a policy; "javaPolicy" is the default provider's standard JCA name
Policy.setPolicy(Policy.getInstance("javaPolicy", null));
// policy loaded; backing config no longer needed
Files.delete(policyPath);
// establish a security manager for enforcing the policy (the default implementation is more than
// sufficient)
System.setSecurityManager(new SecurityManager());
// ...
}
}
В качестве альтернативы, вам придется либо) изменить дистрибутив JRE java.policy
(или укажите другую конфигурацию через policy.url.n
свойства в java.security
) или б) заменить внедрение Системы ClassLoader
с тем, который статически дает AllPermission
к ProtectionDomain
связан с классами, загруженными из "основного" JAR.
Во-вторых, при загрузке Extension
с какой-то JAR, нанять URLClassLoader
подкласс, который а) управляет специфичными для расширения каталогами и б) включает в себя java.io.FilePermission
в коллекции разрешений, статически предоставляемой домену защиты, сопоставленному с его определенными классами. Пример грубой реализации (обратите внимание, что между расширением JAR и каталогом нет постоянной связи; также обратите внимание, что два Extension
s, исходящие из одного JAR (но загруженного разными загрузчиками классов, конечно) получат разные каталоги):
package q46991566;
import java.io.FilePermission;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.cert.Certificate;
import java.util.Enumeration;
import java.util.Objects;
public final class ExtensionLoader extends URLClassLoader {
private static void copyPermissions(PermissionCollection src, PermissionCollection dst) {
for (Enumeration<Permission> e = src.elements(); e.hasMoreElements();) {
dst.add(e.nextElement());
}
}
private final CodeSource origin;
private final PermissionCollection perms = new Permissions();
private final Path baseDir;
public ExtensionLoader(URL extensionOrigin) {
super(new URL[] { extensionOrigin });
origin = new CodeSource(Objects.requireNonNull(extensionOrigin), (Certificate[]) null);
try {
baseDir = Files.createTempDirectory(null);
perms.add(new FilePermission(baseDir.toString().concat("/-"), "read,write,delete"));
copyPermissions(super.getPermissions(origin), perms);
perms.setReadOnly();
}
catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
@Override
protected PermissionCollection getPermissions(CodeSource cs) {
return (origin.implies(cs)) ? perms : super.getPermissions(cs);
}
// ExtensionApiImpl (or ExtensionImpl directly -- but then ExtensionLoader would have to be relocated
// into a separate, also fully privileged JAR, accessible to the extension) can call this to relay to
// extensions where they can persist their data
public Path getExtensionBaseDir() {
return baseDir;
}
// optionally override close() to delete baseDir early
}
Наконец, для непривилегированных Extension
чтобы иметь возможность выполнять привилегированные операции через ExtensionApi
, реализация последнего должна обернуть привилегированный метод (методы выдачи SecurityManager::checkXXX
запросы) вызовы в Privileged(Exception)Action
и передать их AccessController::doPrivileged
; например:
ExtensionApi api = () -> {
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
try {
Files.write(Paths.get("/root/Documents/highly-sensitive.doc"), Collections.singleton("trusted content"),
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
return null;
}
catch (IOException ioe) {
throw new RuntimeException(ioe);
}
});
};
Подробнее о (правильном) использовании "привилегированных блоков" см. AccessController
документация и документ"Руководство по безопасному кодированию для Java SE".