Sandbox terhadap kode berbahaya di aplikasi Java


91

Dalam lingkungan server simulasi di mana pengguna diizinkan untuk mengirimkan kode mereka sendiri untuk dijalankan oleh server, itu jelas akan menguntungkan untuk kode yang dikirim pengguna untuk dijalankan di sisi kotak pasir, tidak seperti Applet yang ada di dalam browser. Saya ingin dapat memanfaatkan JVM itu sendiri, daripada menambahkan lapisan VM lain untuk mengisolasi komponen yang dikirimkan ini.

Batasan semacam ini tampaknya dimungkinkan dengan menggunakan model kotak pasir Java yang sudah ada, tetapi adakah cara dinamis untuk mengaktifkannya hanya untuk bagian yang dikirimkan pengguna dari aplikasi yang sedang berjalan?

Jawaban:


109
  1. Jalankan kode tidak tepercaya di utasnya sendiri. Ini misalnya mencegah masalah dengan loop tak terbatas dan semacamnya, dan membuat langkah selanjutnya lebih mudah. Minta utas utama menunggu utas selesai, dan jika terlalu lama, matikan dengan Thread.stop. Thread.stop tidak digunakan lagi, tetapi karena kode tidak tepercaya seharusnya tidak memiliki akses ke sumber daya apa pun, maka akan aman untuk membunuhnya.

  2. Setel SecurityManager di Thread tersebut. Buat subclass SecurityManager yang menggantikan checkPermission (Permission perm) untuk hanya menampilkan SecurityException untuk semua izin kecuali beberapa yang dipilih. Ada daftar metode dan izin yang mereka butuhkan di sini: Izin di Java TM 6 SDK .

  3. Gunakan ClassLoader kustom untuk memuat kode tidak tepercaya. Pemuat kelas Anda akan dipanggil untuk semua kelas yang digunakan kode tidak tepercaya, sehingga Anda dapat melakukan hal-hal seperti menonaktifkan akses ke setiap kelas JDK. Hal yang harus dilakukan adalah memiliki daftar putih kelas JDK yang diizinkan.

  4. Anda mungkin ingin menjalankan kode tidak tepercaya di JVM terpisah. Meskipun langkah-langkah sebelumnya akan membuat kode aman, ada satu hal menjengkelkan yang masih dapat dilakukan oleh kode yang diisolasi: mengalokasikan memori sebanyak mungkin, yang menyebabkan footprint yang terlihat dari aplikasi utama bertambah.

JSR 121: Spesifikasi API Isolasi Aplikasi dirancang untuk mengatasi hal ini, tetapi sayangnya belum ada penerapannya.

Ini adalah topik yang cukup mendetail, dan saya kebanyakan menulis ini semua dari atas kepala saya.

Tapi bagaimanapun, beberapa kode yang tidak sempurna, gunakan dengan risiko Anda sendiri, mungkin buggy (pseudo):

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

Manajer keamanan

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
}

Benang

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
Kode itu mungkin perlu diperbaiki. Anda tidak bisa benar-benar menjaga ketersediaan JVM. Bersiaplah untuk menghentikan proses (mungkin secara otomatis). Kode masuk ke utas lain - misalnya utas finaliser. Thread.stopakan menyebabkan masalah pada kode pustaka Java. Demikian pula, kode pustaka Java akan membutuhkan izin. Jauh lebih baik untuk memungkinkan SecurityManagerpenggunaan java.security.AccessController. Pemuat kelas mungkin juga harus mengizinkan akses ke kelas kode pengguna itu sendiri.
Tom Hawtin - tackline

4
Mengingat ini adalah topik yang rumit, apakah tidak ada solusi yang ada untuk menangani "plugin" Java dengan cara yang aman?
Nick Spacek

10
Masalah dari pendekatan ini adalah saat Anda menyetel SecurityManager ke Sistem, ini tidak hanya memengaruhi thread yang sedang berjalan, tetapi juga memengaruhi thread lain!
Gelin Luo

2
Maaf, tapi thread.stop () bisa ditangkap dengan throwable. Anda bisa while (thread.isAlive) Thread.stop (), tapi kemudian saya bisa memanggil fungsi rekursif yang menangkap pengecualian. Diuji pada pc saya, fungsi rekursif menang atas stop (). Sekarang Anda memiliki utas sampah, mencuri cpu dan sumber daya
Lesto

9
Selain fakta yang System.setSecurityManager(…)akan mempengaruhi seluruh JVM, tidak hanya thread yang memanggil metode tersebut, gagasan untuk membuat keputusan keamanan berdasarkan thread telah ditinggalkan ketika Java dialihkan dari 1.0 ke 1.1. Saat ini diketahui bahwa kode tidak tepercaya dapat memanggil kode tepercaya dan sebaliknya, terlepas dari utas mana yang menjalankan kode tersebut. Tidak ada pengembang yang harus mengulangi kesalahan tersebut.
Holger

18

Jelas skema seperti itu menimbulkan segala macam masalah keamanan. Java memiliki kerangka keamanan yang ketat, tetapi tidak sepele. Kemungkinan mengacaukannya dan membiarkan pengguna yang tidak berhak mengakses komponen sistem vital tidak boleh diabaikan.

Selain peringatan itu, jika Anda mengambil input pengguna dalam bentuk kode sumber, hal pertama yang perlu Anda lakukan adalah mengompilasinya ke bytecode Java. AFIAK, ini tidak dapat dilakukan secara native, jadi Anda harus melakukan panggilan sistem ke javac, dan mengompilasi kode sumber ke bytecode pada disk. Berikut tutorial yang bisa digunakan sebagai titik awal untuk ini. Sunting : seperti yang saya pelajari di komentar, Anda sebenarnya dapat mengkompilasi kode Java dari sumber asli menggunakan javax.tools.JavaCompiler

Setelah Anda memiliki JVM bytecode, Anda dapat memuat ke dalam JVM menggunakan ClassLoader ini defineClass fungsi. Untuk mengatur konteks keamanan untuk kelas yang dimuat ini, Anda perlu menentukan ProtectionDomain . Konstruktor minimal untuk ProtectionDomain membutuhkan CodeSource dan PermissionCollection . PermissionCollection adalah objek penggunaan utama Anda di sini- Anda dapat menggunakannya untuk menentukan izin yang tepat yang dimiliki kelas yang dimuat. Izin ini pada akhirnya harus diberlakukan oleh AccessController JVM .

Ada banyak kemungkinan titik kesalahan di sini, dan Anda harus sangat berhati-hati untuk sepenuhnya memahami semuanya sebelum menerapkan apa pun.


2
Kompilasi Java cukup mudah menggunakan API javax.tools JDK 6.
Alan Krueger

10

The Java-Sandbox adalah perpustakaan untuk mengeksekusi kode Java dengan seperangkat terbatas perizinan. Ini dapat digunakan untuk mengizinkan akses hanya ke sekumpulan kelas dan sumber daya yang terdaftar dalam daftar putih. Tampaknya tidak dapat membatasi akses ke metode individu. Ini menggunakan sistem dengan pemuat kelas khusus dan pengelola keamanan untuk mencapai ini.

Saya belum pernah menggunakannya tetapi tampilannya dirancang dengan baik dan didokumentasikan dengan cukup baik.

@waqas telah memberikan jawaban yang sangat menarik yang menjelaskan bagaimana ini mungkin untuk Anda terapkan. Tetapi jauh lebih aman untuk menyerahkan kode keamanan yang kritis dan kompleks kepada para ahli.

Perhatikan bahwa proyek tersebut belum diperbarui sejak 2013 dan pembuatnya menggambarkannya sebagai "eksperimental". Halaman beranda telah menghilang tetapi entri Source Forge tetap ada.

Kode contoh diadaptasi dari situs web proyek:

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

Untuk mengatasi masalah dalam jawaban yang diterima di mana kustom SecurityManagerakan berlaku untuk semua utas di JVM, bukan pada basis per utas, Anda dapat membuat ubahsuaian SecurityManageryang dapat diaktifkan / dinonaktifkan untuk utas tertentu sebagai berikut:

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

}

ToggleSecurirtyManagerPermissionhanyalah implementasi sederhana java.security.Permissionuntuk memastikan bahwa hanya kode resmi yang dapat mengaktifkan / menonaktifkan pengelola keamanan. Ini terlihat seperti ini:

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

}


Penggunaan ThreadLocal yang sangat cerdas untuk membuat SecurityManager dengan cakupan sistem secara efektif dengan cakupan thread (yang diinginkan sebagian besar pengguna). Pertimbangkan juga untuk menggunakan InheritableThreadLocal untuk mengirimkan otomatis properti terlarang ke utas yang dihasilkan oleh kode tidak tepercaya.
Nick

4

Sudah sangat terlambat untuk memberikan saran atau solusi, tetapi saya masih menghadapi masalah serupa, lebih berorientasi pada penelitian. Pada dasarnya saya berusaha memberikan bekal dan evaluasi otomatis untuk tugas pemrograman mata kuliah Java di platform e-learning.

  1. salah satu caranya adalah, Buat mesin virtual terpisah (bukan JVM) tetapi mesin virtual aktual dengan konfigurasi minimum yang memungkinkan OS untuk setiap siswa.
  2. Instal JRE untuk Java atau pustaka sesuai dengan bahasa pemrograman Anda, mana pun yang Anda inginkan agar dikompilasi dan dijalankan oleh siswa di mesin ini.

Saya tahu ini terdengar cukup rumit dan banyak tugas, tetapi Oracle Virtual Box sudah menyediakan Java API untuk membuat atau mengkloning mesin virtual secara dinamis. https://www.virtualbox.org/sdkref/index.html (Catatan, bahkan VMware juga menyediakan API untuk melakukan hal yang sama)

Dan untuk ukuran minimum dan konfigurasi distribusi Linux Anda dapat merujuk ke yang ini di sini http://www.slitaz.org/en/ ,

Jadi sekarang jika siswa mengacaukan atau mencoba melakukannya, mungkin dengan memori atau sistem file atau jaringan, soket, maksimal dia dapat merusak VM miliknya sendiri.

Juga secara internal ke dalam VM ini, Anda dapat memberikan keamanan tambahan seperti Sandbox (manajer keamanan) untuk Java atau membuat akun khusus pengguna di Linux dan dengan demikian membatasi akses.

Semoga ini membantu !!


3

Berikut solusi aman untuk masalah ini:

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.

}

Tolong beri komentar anda!

CU

Arno


Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.