Как создать безопасную песочницу JEXL?

Я создаю изолированную программную среду для выполнения сценариев JEXL, чтобы злонамеренный пользователь не мог получить доступ к данным вне переменных, к которым мы им предоставили доступ, а также не мог выполнить DOS-атаку на сервер. Я хотел бы задокументировать это для всех, кто также делает это, а также получить вклад других людей в этот подход.

Ниже приведен список вещей, о которых я знаю, которые необходимо решить:

  1. Разрешить только создание экземпляров классов с использованием 'new', которые находятся в белом списке.
  2. Не разрешайте доступ к методу getClass в любом классе, потому что тогда можно вызвать forName и получить доступ к любому классу.
  3. Ограничить доступ к ресурсам, таким как файлы.
  4. Разрешить выражению только определенное количество времени для выполнения, чтобы мы могли ограничить количество ресурсов, которые оно потребляет.

Это не относится к JEXL, но может применяться к языку сценариев, который вы используете:

  1. Не разрешайте объекту иметь собственный метод finalize, потому что метод finalize вызывается из потока финализатора и будет выполняться с исходным AccessControlContext вместо того, который используется для создания объекта и выполнения кода в нем.

1 ответ

Решение

Вот мой подход к решению каждого из этих случаев. Я создал модульные тесты для проверки каждого из этих случаев, и я убедился, что они работают.

  1. JEXL делает это довольно легко. Просто создайте собственный ClassLoader. Переопределите два метода loadClass(). На JexlEngine вызовите setClassLoader().

  2. Опять же, JEXL делает это довольно легко. Вы должны заблокировать как '.class', так и '.getClass()'. Создайте свой собственный класс Uberspect, который расширяет UberspectImpl. Переопределите getPropertyGet, если идентификатор равен "классу" и возвращает ноль. Переопределите getMethod, если метод равен "getClass" и возвращает ноль. При создании JexlEngine передайте ссылку на вашу реализацию Uberspect.

  3. Вы делаете это с помощью механизма AccessController в Java. Я сделаю это быстро. Запустите Java с -Djava.security.policy=policyfile. Создайте файл с именем policyfile, содержащий эту строку: grant {разрешение java.security.AllPermission; }; Установите SecurityManager по умолчанию с помощью этого вызова: System.setSecurityManager(new SecurityManager()); Теперь вы можете контролировать разрешения, и ваше приложение по умолчанию имеет все разрешения. Было бы лучше, если бы вы ограничивали разрешения вашего приложения только тем, что ему требуется. Затем создайте AccessControlContext, который ограничивает разрешения до минимума, и вызовите AccessController.doPrivileged() и передайте AccessControlContext, затем выполните сценарий JEXL внутри doPrivileged(). Вот небольшая программа, которая демонстрирует это. Сценарий JEXL вызывает System.exit(1), и если он не обернут в doPrivileged (), он успешно завершит работу JVM.

    System.out.println("java.security.policy=" + System.getProperty("java.security.policy"));
    System.setSecurityManager(new SecurityManager());
    try {
        Permissions perms = new Permissions();
        perms.add(new RuntimePermission("accessDeclaredMembers"));
        ProtectionDomain domain = new ProtectionDomain(new CodeSource( null, (Certificate[]) null ), perms );
        AccessControlContext restrictedAccessControlContext = new AccessControlContext(new ProtectionDomain[] { domain } );
    
        JexlEngine jexlEngine = new JexlEngine();
        final Script finalExpression = jexlEngine.createScript(
                "i = 0; intClazz = i.class; "
                + "clazz = intClazz.forName(\"java.lang.System\"); "
                + "m = clazz.methods; m[0].invoke(null, 1); c");
    
        AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
            @Override
            public Object run() throws Exception {
                return finalExpression.execute(new MapContext());
            }
        }, restrictedAccessControlContext);
    }
    catch (Throwable ex) {
        ex.printStackTrace();
    }
    
  4. Уловка с этим прерывает сценарий до его завершения. Один из способов сделать это - создать собственный класс JexlArithmetic. Затем переопределите каждый метод в этом классе и перед вызовом реального метода в суперклассе проверьте, должен ли скрипт прекратить выполнение. Я использую ExecutorService для создания потоков. Когда вызывается Future.get(), укажите время ожидания. Если выбрасывается TimeoutException, вызовите Future.cancel(), который прерывает поток, выполняющий скрипт. Внутри каждого переопределенного метода в новом классе JexlArithmetic проверяйте Thread.interrupted() и, если это правда, генерируйте java.util.concurrent.CancellationException. Есть ли лучшее место для размещения кода, который будет выполняться регулярно при выполнении скрипта, чтобы его можно было прервать?

Другие вопросы по тегам