Javaアプリケーションの悪意のあるコードに対するサンドボックス


91

ユーザーがサーバーで実行する独自のコードを送信できるシミュレーションサーバー環境では、ユーザーが送信したコードをサンドボックスのサイドで実行すると、アプレットがブラウザー内にあるのとは異なり、明らかに有利です。これらのサブミットされたコンポーネントを分離するために別のVMレイヤーを追加するのではなく、JVM自体を活用できるようにしたかったのです。

この種の制限は、既存のJavaサンドボックスモデルを使用して可能であるように見えますが、実行中のアプリケーションのユーザーが送信した部分だけでそれを有効にする動的な方法はありますか?

回答:


109
  1. 独自のスレッドで信頼できないコードを実行します。これにより、たとえば無限ループなどの問題が回避され、将来のステップが容易になります。メインスレッドにスレッドの終了を待機させます。時間がかかりすぎる場合は、Thread.stopで強制終了します。Thread.stopは非推奨ですが、信頼されていないコードはリソースにアクセスできないため、強制終了しても安全です。

  2. そのスレッドにSecurityManagerを設定します。checkPermission(Permission perm)をオーバーライドするSecurityManagerのサブクラスを作成して、一部を除くすべての権限に対してSecurityExceptionをスローするだけです。メソッドとそれらが必要とするアクセス権のリストは、Java TM 6 SDKのアクセス権です。

  3. カスタムClassLoaderを使用して、信頼できないコードをロードします。信頼できないコードが使用するすべてのクラスに対してクラスローダーが呼び出されるため、個々のJDKクラスへのアクセスを無効にするなどの操作を実行できます。すべきことは、許可されたJDKクラスのホワイトリストを用意することです。

  4. 信頼できないコードを別のJVMで実行したい場合があります。前の手順でコードを安全にすることができますが、分離コードで実行できる厄介なことが1つあります。可能な限り多くのメモリを割り当てることです。これにより、メインアプリケーションの目に見えるフットプリントが大きくなります。

JSR 121:アプリケーション分離API仕様はこれを解決するために設計されましたが、残念ながらまだ実装されていません。

これはかなり詳細なトピックであり、私はこれをほとんど頭の上の部分から書きます。

しかしとにかく、いくつかの不完全な、自分の責任で使用する、おそらくバグのある(疑似)コード:

ClassLoader

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
  }
}

保安管理者

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
}

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) {}
  }
}

4
そのコードはいくつかの作業が必要になる場合があります。JVMの可用性を実際に保護することはできません。プロセスを強制終了する準備をしてください(おそらく自動的に)。コードは他のスレッドに移ります-例えばファイナライザスレッド。Thread.stopJavaライブラリコードで問題が発生します。同様に、Javaライブラリコードには権限が必要です。SecurityManagerが使用できるようにするための はるかに良いjava.security.AccessController。クラスローダーは、おそらくユーザーコード自身のクラスへのアクセスも許可するべきです。
トム・ホーティン-タックライン2010

3
これが非常に複雑なテーマであることを考えると、Javaの「プラグイン」を安全に処理するための既存のソリューションはありませんか?
Nick Spacek

9
このアプローチの問題は、SecurityManagerをSystemに設定すると、実行中のスレッドだけでなく、他のスレッドにも影響を与えることです。
Gelin Luo 2013

2
申し訳ありませんが、thread.stop()はスロー可能オブジェクトでキャッチできます。while(thread.isAlive)Thread.stop()を実行できますが、例外をキャッチする関数を再帰的に呼び出すことができます。私のPCでテストしたところ、再帰関数はstop()に勝っています。これで、ガベージスレッドがあり、CPUとリソースを盗む
Lesto

8
そのSystem.setSecurityManager(…)メソッドを呼び出すスレッドだけでなく、JVM全体に影響するという事実に加えて、Javaが1.0から1.1に切り替わったときに、スレッドに基づいてセキュリティの決定を行うという考え方は放棄されました。現時点では、どのスレッドがコードを実行するかに関係なく、信頼されていないコードが信頼されたコードを呼び出す可能性があること、およびその逆が認識されています。開発者は間違いを繰り返すべきではありません。
Holger

18

明らかに、そのようなスキームはあらゆる種類のセキュリティ上の懸念を引き起こします。Javaには厳密なセキュリティフレームワークがありますが、それは簡単なことではありません。それを台無しにし、権限のないユーザーが重要なシステムコンポーネントにアクセスできるようにする可能性を見落としてはいけません。

その警告はさておき、ユーザー入力をソースコードの形式で受け取っている場合、最初に行う必要があるのは、それをJavaバイトコードにコンパイルすることです。AFIAK、これはネイティブでは実行できないため、javacへのシステムコールを実行し、ソースコードをディスク上のバイトコードにコンパイルする必要があります。ここだ、このための開始点として使用することができるチュートリアルが。 編集:コメントで学んだように、実際にはソースからjava.tools.JavaCompilerを使用してJavaコードをコンパイルできます

JVMバイトコードを取得したら、ClassLoaderの defineClass関数を使用してそれをJVMにロードできます。この読み込まれたクラスのセキュリティコンテキストを設定するには、ProtectionDomainを指定する必要があります。以下のための最小限のコンストラクタのProtectionDomainはのCodeSourceとの両方が必要ですのPermissionCollectionを。PermissionCollectionは、ここで主に使用するオブジェクトです。これを使用して、ロードされたクラスが持つ正確なアクセス許可を指定できます。これらの権限は、最終的にはJVMのAccessControllerによって適用されます

ここには多くの考えられるエラーのポイントがあり、何かを実装する前にすべてを完全に理解するように細心の注意を払う必要があります。


2
JDK 6のjavax.tools APIを使用すると、Javaコンパイルはかなり簡単です。
アランクルーガー、

10

Javaにサンドボックスは、アクセス権の限られたセットを使用してJavaコードを実行するためのライブラリです。ホワイトリストに登録されたクラスとリソースのセットへのアクセスのみを許可するために使用できます。個々のメソッドへのアクセスを制限することはできないようです。これは、カスタムクラスローダーとセキュリティマネージャーを備えたシステムを使用してこれを実現します。

私はそれを使用していませんが、うまく設計され、かなり文書化されています。

@waqasは、これを自分で実装する方法を説明する非常に興味深い回答を提供しています。しかし、そのようなセキュリティの重要で複雑なコードを専門家に任せる方がはるかに安全です。

ただし、プロジェクトは2013年以降更新されておらず、作成者はそれを「実験的」と表現していることに注意してください。そのホームページは消えましたが、Source Forgeのエントリーは残っています。

プロジェクトのWebサイトから変更されたサンプルコード:

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());

4

カスタムSecurityManagerがスレッドごとではなく、JVMのすべてのスレッドに適用されるという承認された回答の問題に対処するために、SecurityManager次のように特定のスレッドに対して有効/無効にすることができるカスタムを作成できます。

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();
  }

}

ToggleSecurirtyManagerPermissionjava.security.Permission承認されたコードのみがセキュリティマネージャを有効/無効にできるようにするためのの単純な実装です。次のようになります。

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 "";
  }

}


ThreadLocalを非常に賢く使用して、システムスコープのSecurityManagerを効果的にスレッドスコープにします(ほとんどのユーザーが望んでいます)。また、InheritableThreadLocalを使用して、許可されていないプロパティを信頼できないコードによって生成されたスレッドに自動送信することも検討してください。
ニック

4

さて、提案や解決策を提供するのは非常に遅いですが、それでも私は同様の問題に直面していました。基本的に私は、eラーニングプラットフォームでのJavaコースのプログラミング割り当てのプロビジョニングと自動評価を提供しようとしていました。

  1. 1つの方法としては、個別の仮想マシン(JVMではなく)を作成しますが、実際の仮想マシンは、学生ごとに可能な最小構成のOSを使用します。
  2. プログラミング言語に応じて、Javaまたはライブラリ用のJREをインストールします。学生がこれらのマシンでコンパイルおよび実行したい方をインストールします。

これは非常に複雑で多くのタスクのように聞こえますが、Oracle Virtual Boxはすでに仮想マシンを動的に作成または複製するJava APIを提供しています。 https://www.virtualbox.org/sdkref/index.html(注、VMwareでも同じことを行うためのAPIを提供しています)

また、最小サイズと構成のLinuxディストリビューションについては、こちらhttp://www.slitaz.org/en/を参照してください

だから今、学生がそれをめちゃくちゃにしたりしようとしたりした場合、メモリやファイルシステム、ネットワーク、ソケットなどが原因で、最大で自分のVMを損傷する可能性があります。

また、これらのVMの内部では、Javaのサンドボックス(セキュリティマネージャー)などの追加のセキュリティを提供したり、Linuxでユーザー固有のアカウントを作成してアクセスを制限したりできます。

お役に立てれば !!


3

この問題に対するスレッドセーフなソリューションは次のとおりです。

https://svn.code.sf.net/p/loggifier/code/trunk/de.unkrig.commons.lang/src/de/unkrig/commons/lang/security/Sandbox.java

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.

}

コメントしてください!

CU

アルノ


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.