Bac à sable contre le code malveillant dans une application Java
dans un environnement de serveur de simulation où les utilisateurs sont autorisés à soumettre leur propre code pour être exécuté par le serveur, il serait clairement avantageux pour tout code soumis par l'utilisateur d'être exécuté dans un bac à sable latéral, un peu comme les Applets sont dans un navigateur. Je voulais être capable de tirer parti de la JVM elle-même, plutôt que d'ajouter une autre couche VM pour isoler ces composants soumis.
ce type de limitation semble être possible en utilisant le modèle Java sandbox existant, mais est y a-t-il un moyen dynamique d'activer cela uniquement pour les parties soumises par l'utilisateur d'une application en cours d'exécution?
7 réponses
-
exécutez le code non fiable dans son propre fil. Cela évite par exemple les problèmes avec les boucles infinies et autres, et rend les étapes futures plus faciles. Demandez au fil principal d'attendre que le fil se termine, et si cela prend trop de temps, tuez-le avec du fil.arrêter. Fil.stop est déprécié, mais comme le code non fiable ne devrait pas avoir accès à des ressources, il serait sans danger de le tuer.
-
Définir un SecurityManager sur ce fil. Créer une sous-classe de SecurityManager qui remplace checkPermission(Perm De Permission) pour simplement lancer un SecurityException pour toutes les permissions sauf quelques rares. Il y a une liste de méthodes et les permissions qu'ils exigent ici: Permissions dans le Java TM 6 SDK .
-
utilisez un ClassLoader personnalisé pour charger le code non fiable. Votre chargeur de classe serait appelé pour toutes les classes que le code non fiable utilise, de sorte que vous pouvez faire des choses comme désactiver l'accès aux classes JDK individuelles. La chose à faire est d'avoir une liste blanche des classes JDK autorisées.
-
vous pourriez utiliser le code non fiable dans une JVM séparée. Alors que les étapes précédentes rendraient le code sûr, il y a une chose ennuyeuse que le code isolé peut encore faire: allouer autant de mémoire que possible, ce qui provoque l'empreinte visible de l'application principale pour grandir.
JSR 121: Application Isolation API Spécification a été conçu pour résoudre ce, mais malheureusement, il n'a pas encore une mise en œuvre.
c'est un sujet assez détaillé, et j'écris surtout tout ça sur le dessus de ma tête.
mais de toute façon, certains imparfaits, utiliser-à-vos-risques, probablement code buggy (pseudo) :
chargeur de classe
class MyClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name is white-listed JDK class) return super.loadClass(name);
return findClass(name);
}
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the untrusted class data here
}
}
SecurityManager
class MySecurityManager extends SecurityManager {
private Object secret;
public MySecurityManager(Object pass) { secret = pass; }
private void disable(Object pass) {
if (pass == secret) secret = null;
}
// ... override checkXXX method(s) here.
// Always allow them to succeed when secret==null
}
Thread
class MyIsolatedThread extends Thread {
private Object pass = new Object();
private MyClassLoader loader = new MyClassLoader();
private MySecurityManager sm = new MySecurityManager(pass);
public void run() {
SecurityManager old = System.getSecurityManager();
System.setSecurityManager(sm);
runUntrustedCode();
sm.disable(pass);
System.setSecurityManager(old);
}
private void runUntrustedCode() {
try {
// run the custom class's main method for example:
loader.loadClass("customclassname")
.getMethod("main", String[].class)
.invoke(null, new Object[]{...});
} catch (Throwable t) {}
}
}
de toute évidence, un tel stratagème soulève toutes sortes de préoccupations en matière de sécurité. Java a un cadre de sécurité rigoureux, mais ce n'est pas anodin. La possibilité de tout faire foirer et de laisser un utilisateur non privilégié accéder aux composants essentiels du système ne devrait pas être négligée.
cet avertissement mis à part, si vous prenez l'entrée de l'utilisateur sous la forme de code source, la première chose que vous devez faire est de le compiler en Java bytecode. AFIAK, cela ne peut pas être fait nativement, donc vous aurez besoin de faire un système appeler javac, et compiler le code source en bytecode sur le disque. Voici un tutoriel qui peut être utilisé comme point de départ pour cela. Edit : comme je l'ai appris dans les commentaires, vous pouvez en fait compiler le code Java à partir de la source nativement en utilisant javax.outils.JavaCompiler
une fois que vous avez le bytecode JVM, vous pouvez le charger dans le JVM en utilisant un Classloader's defineClass de la fonction. Pour définir un contexte de sécurité pour cette classe chargée, vous devrez spécifier un ProtectionDomain . Le constructeur minimal pour un Domaine De Protection nécessite à la fois une source de code et une collecte de permissions . Le PermissionCollection est l'objet de l'utilisation principale pour vous ici - vous pouvez l'utiliser pour spécifier les autorisations exactes de la classe chargée. Ces autorisations devraient en fin de Compte être appliquées par les AccessController .
il y a beaucoup de points d'erreur possibles ici, et vous devriez être extrêmement prudent pour tout comprendre complètement avant de mettre en œuvre quoi que ce soit.
le Java-Sandbox est une bibliothèque pour exécuter du code Java avec un ensemble limité de permissions. Il peut être utilisé pour ne permettre l'accès qu'à un ensemble de classes et de ressources figurant sur la liste blanche. Il ne semble pas pouvoir restreindre l'accès à des méthodes individuelles. Il utilise un système avec un chargeur de classe et gestionnaire de sécurité pour y parvenir.
Je ne l'ai pas utilisé mais il semble bien conçu et raisonnablement bien documenté.
@waqas a donné une réponse très intéressante expliquant comment cela est possible de mettre en œuvre vous-même. Mais il est beaucoup plus sûr de laisser ce code de sécurité critique et complexe aux experts.
note cependant que le projet n'a pas été mis à jour depuis 2013 et les créateurs le décrivent comme"expérimental". Sa page d'accueil a disparu, mais l'entrée de la source demeure.
exemple de code adapté du site Web du projet:
SandboxService sandboxService = SandboxServiceImpl.getInstance();
// Configure context
SandboxContext context = new SandboxContext();
context.addClassForApplicationLoader(getClass().getName());
context.addClassPermission(AccessType.PERMIT, "java.lang.System");
// Whithout this line we get a SandboxException when touching System.out
context.addClassPermission(AccessType.PERMIT, "java.io.PrintStream");
String someValue = "Input value";
class TestEnvironment implements SandboxedEnvironment<String> {
@Override
public String execute() throws Exception {
// This is untrusted code
System.out.println(someValue);
return "Output value";
}
};
// Run code in sandbox. Pass arguments to generated constructor in TestEnvironment.
SandboxedCallResult<String> result = sandboxService.runSandboxed(TestEnvironment.class,
context, this, someValue);
System.out.println(result.get());
Eh bien, il est très tard pour donner des suggestions ou des solutions, mais j'étais quand même confronté à un genre de problème similaire, plus orienté vers la recherche. Fondamentalement, j'ai essayé de fournir une disposition et des évaluations automatiques pour les affectations de programmation pour le cours de Java dans les plates-formes d'apprentissage en ligne.
- une façon pourrait être, créer des machines virtuelles séparées (pas JVM) mais des machines virtuelles réelles avec un minimum de configuration OS possibles pour chaque élève.
- installez JRE pour Java ou les bibliothèques selon vos langages de programmation, selon ce que vous voulez que les étudiants compilent et exécutent sur ces machines.
je sais que cela semble assez complexe et beaucoup de tâches, mais Oracle Virtual Box fournit déjà une API Java pour créer ou cloner des machines virtuelles de manière dynamique. https://www.virtualbox.org/sdkref/index.html (notez que même VMware fournit également une API pour faire la même chose)
Et pour la taille minimale et la configuration de la distribution Linux, vous pouvez vous référer à celle-ci ici http://www.slitaz.org/en / ,
donc maintenant si les étudiants ratent ou tentent de le faire, peut-être avec la mémoire ou le système de fichiers ou le réseau, socket, maximum il peut endommager sa propre VM.
aussi en interne dans ces VM vous pouvez fournir une sécurité supplémentaire comme Sandbox (gestionnaire de sécurité ) pour Java ou la création de comptes utilisateur spécifiques sur Linux et ainsi, la restriction de l'accès.
Espérons que cette aide !!
Voici une solution sans fil pour le problème:
package de.unkrig.commons.lang.security;
import java.security.AccessControlContext;
import java.security.Permission;
import java.security.Permissions;
import java.security.ProtectionDomain;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import de.unkrig.commons.nullanalysis.Nullable;
/**
* This class establishes a security manager that confines the permissions for code executed through specific classes,
* which may be specified by class, class name and/or class loader.
* <p>
* To 'execute through a class' means that the execution stack includes the class. E.g., if a method of class {@code A}
* invokes a method of class {@code B}, which then invokes a method of class {@code C}, and all three classes were
* previously {@link #confine(Class, Permissions) confined}, then for all actions that are executed by class {@code C}
* the <i>intersection</i> of the three {@link Permissions} apply.
* <p>
* Once the permissions for a class, class name or class loader are confined, they cannot be changed; this prevents any
* attempts (e.g. of the confined class itself) to release the confinement.
* <p>
* Code example:
* <pre>
* Runnable unprivileged = new Runnable() {
* public void run() {
* System.getProperty("user.dir");
* }
* };
*
* // Run without confinement.
* unprivileged.run(); // Works fine.
*
* // Set the most strict permissions.
* Sandbox.confine(unprivileged.getClass(), new Permissions());
* unprivileged.run(); // Throws a SecurityException.
*
* // Attempt to change the permissions.
* {
* Permissions permissions = new Permissions();
* permissions.add(new AllPermission());
* Sandbox.confine(unprivileged.getClass(), permissions); // Throws a SecurityException.
* }
* unprivileged.run();
* </pre>
*/
public final
class Sandbox {
private Sandbox() {}
private static final Map<Class<?>, AccessControlContext>
CHECKED_CLASSES = Collections.synchronizedMap(new WeakHashMap<Class<?>, AccessControlContext>());
private static final Map<String, AccessControlContext>
CHECKED_CLASS_NAMES = Collections.synchronizedMap(new HashMap<String, AccessControlContext>());
private static final Map<ClassLoader, AccessControlContext>
CHECKED_CLASS_LOADERS = Collections.synchronizedMap(new WeakHashMap<ClassLoader, AccessControlContext>());
static {
// Install our custom security manager.
if (System.getSecurityManager() != null) {
throw new ExceptionInInitializerError("There's already a security manager set");
}
System.setSecurityManager(new SecurityManager() {
@Override public void
checkPermission(@Nullable Permission perm) {
assert perm != null;
for (Class<?> clasS : this.getClassContext()) {
// Check if an ACC was set for the class.
{
AccessControlContext acc = Sandbox.CHECKED_CLASSES.get(clasS);
if (acc != null) acc.checkPermission(perm);
}
// Check if an ACC was set for the class name.
{
AccessControlContext acc = Sandbox.CHECKED_CLASS_NAMES.get(clasS.getName());
if (acc != null) acc.checkPermission(perm);
}
// Check if an ACC was set for the class loader.
{
AccessControlContext acc = Sandbox.CHECKED_CLASS_LOADERS.get(clasS.getClassLoader());
if (acc != null) acc.checkPermission(perm);
}
}
}
});
}
// --------------------------
/**
* All future actions that are executed through the given {@code clasS} will be checked against the given {@code
* accessControlContext}.
*
* @throws SecurityException Permissions are already confined for the {@code clasS}
*/
public static void
confine(Class<?> clasS, AccessControlContext accessControlContext) {
if (Sandbox.CHECKED_CLASSES.containsKey(clasS)) {
throw new SecurityException("Attempt to change the access control context for '" + clasS + "'");
}
Sandbox.CHECKED_CLASSES.put(clasS, accessControlContext);
}
/**
* All future actions that are executed through the given {@code clasS} will be checked against the given {@code
* protectionDomain}.
*
* @throws SecurityException Permissions are already confined for the {@code clasS}
*/
public static void
confine(Class<?> clasS, ProtectionDomain protectionDomain) {
Sandbox.confine(
clasS,
new AccessControlContext(new ProtectionDomain[] { protectionDomain })
);
}
/**
* All future actions that are executed through the given {@code clasS} will be checked against the given {@code
* permissions}.
*
* @throws SecurityException Permissions are already confined for the {@code clasS}
*/
public static void
confine(Class<?> clasS, Permissions permissions) {
Sandbox.confine(clasS, new ProtectionDomain(null, permissions));
}
// Code for 'CHECKED_CLASS_NAMES' and 'CHECKED_CLASS_LOADERS' omitted here.
}
s'il vous Plaît commentaire!
CU
Arno
pour résoudre le problème dans la réponse acceptée où la coutume SecurityManager
s'appliquera à tous les threads dans la JVM, plutôt que sur une base de per-thread, vous pouvez créer une coutume SecurityManager
qui peut être activée/désactivée pour les threads spécifiques comme suit:
import java.security.Permission;
public class SelectiveSecurityManager extends SecurityManager {
private static final ToggleSecurityManagerPermission TOGGLE_PERMISSION = new ToggleSecurityManagerPermission();
ThreadLocal<Boolean> enabledFlag = null;
public SelectiveSecurityManager(final boolean enabledByDefault) {
enabledFlag = new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return enabledByDefault;
}
@Override
public void set(Boolean value) {
SecurityManager securityManager = System.getSecurityManager();
if (securityManager != null) {
securityManager.checkPermission(TOGGLE_PERMISSION);
}
super.set(value);
}
};
}
@Override
public void checkPermission(Permission permission) {
if (shouldCheck(permission)) {
super.checkPermission(permission);
}
}
@Override
public void checkPermission(Permission permission, Object context) {
if (shouldCheck(permission)) {
super.checkPermission(permission, context);
}
}
private boolean shouldCheck(Permission permission) {
return isEnabled() || permission instanceof ToggleSecurityManagerPermission;
}
public void enable() {
enabledFlag.set(true);
}
public void disable() {
enabledFlag.set(false);
}
public boolean isEnabled() {
return enabledFlag.get();
}
}
ToggleSecurirtyManagerPermission
est une simple implémentation de java.security.Permission
pour s'assurer que seul le code autorisé peut activer/désactiver le gestionnaire de sécurité. Il ressemble à ceci:
import java.security.Permission;
public class ToggleSecurityManagerPermission extends Permission {
private static final long serialVersionUID = 4812713037565136922L;
private static final String NAME = "ToggleSecurityManagerPermission";
public ToggleSecurityManagerPermission() {
super(NAME);
}
@Override
public boolean implies(Permission permission) {
return this.equals(permission);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ToggleSecurityManagerPermission) {
return true;
}
return false;
}
@Override
public int hashCode() {
return NAME.hashCode();
}
@Override
public String getActions() {
return "";
}
}
Vous aurez probablement besoin d'utiliser un SecurityManger et/ou AccessController . Pour plus de détails, voir Java Security Architecture et autres documents de sécurité de Sun.