Как создать безопасную тестовую среду JEXL?

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

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

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

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

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

Ответ 1

ОБНОВЛЕНИЕ: Все это было сделано с использованием JEXL 2.0.1. Возможно, вам придется адаптировать это, чтобы заставить его работать с более новыми версиями.

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

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

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

    class MyUberspectImpl extends UberspectImpl {
    
        public MyUberspectImpl(Log jexlLog) {
            super(jexlLog);
        }
    
        @Override
        public JexlPropertyGet getPropertyGet(Object obj, Object identifier, JexlInfo info) {
            // for security we do not allow access to .class property
            if ("class".equals(identifier)) throw new RuntimeException("Access to getClass() method is not allowed");
            JexlPropertyGet propertyGet = super.getPropertyGet(obj, identifier, info);
            return propertyGet;
        }
    
        @Override
        public JexlMethod getMethod(Object obj, String method, Object[] args, JexlInfo info) {
            // for security we do not allow access to .getClass() method
            if ("getClass".equals(method)) throw new RuntimeException("Access to getClass() method is not allowed");
            return super.getMethod(obj, method, args, info);
        }
    
    }
    
  3. Вы делаете это с помощью механизма Java AccessController. Я сделаю это быстро. Запустите 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. Есть ли лучшее место для размещения кода, который будет выполняться регулярно при выполнении скрипта, чтобы его можно было прервать?

Вот выдержка из класса MyJexlArithmetic. Вы должны добавить все остальные методы:

    public class MyJexlArithmetic extends JexlArithmetic {

        public MyJexlArithmetic(boolean lenient) {
            super(lenient);
        }

        private void checkInterrupted() {
            if (Thread.interrupted()) throw new CancellationException();
        }

        @Override
        public boolean equals(Object left, Object right) {
            checkInterrupted();
            return super.equals(left, right); //To change body of generated methods, choose Tools | Templates.
        }

        @Override
        public Object add(Object left, Object right) {
            checkInterrupted();
            return super.add(left, right);
        }
    }

Вот как я запускаю JexlEngine:

        Log jexlLog = LogFactory.getLog("JEXL");
        Map <String, Object> functions = new HashMap();
        jexlEngine = new JexlEngine(new MyUberspectImpl(jexlLog), new MyJexlArithmetic(false), functions, jexlLog);