JEP 415: Context-Specific Deserialization Filters
Контекстно-зависимые фильтры десериализации
| Ответственный | Roger Riggs |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 17 |
| Компонент | core-libs / java.io:serialization |
| Обсуждение | core dash libs dash dev at openjdk dot java dot net |
| Трудоёмкость | S |
| Длительность | S |
| Связан с | JEP 290: Filter Incoming Serialization Data |
| Рецензенты | Brian Goetz, Chris Hegarty |
| Одобрен | Brian Goetz |
| Создан | 2021/03/10 15:36 |
| Обновлён | 2022/04/08 14:05 |
| Задача | 8263381 |
Аннотация
Дать приложениям возможность настраивать контекстно-зависимые и выбираемые динамически фильтры десериализации с помощью общей для всей JVM фабрики фильтров, которая вызывается, чтобы выбрать фильтр для каждой отдельной операции десериализации.
Что не является целью
-
Определение политик выбора фильтров десериализации не является целью.
-
Определение механизма настройки или распространения фильтров не является целью.
Мотивация
Десериализация недоверенных данных опасна по своей природе, потому что содержимое входящего потока данных определяет, какие объекты создаются, какие значения имеют их поля и какие ссылки связывают их между собой. Во многих типичных случаях байты потока приходят от неизвестного, недоверенного или неаутентифицированного клиента. Тщательно сконструировав поток, злоумышленник может добиться выполнения кода в произвольных классах со злым умыслом. Если у создания объекта есть побочные эффекты, которые меняют состояние или вызывают другие действия, эти действия могут нарушить целостность объектов приложения, объектов библиотек и даже среды выполнения Java. Главный способ предотвратить атаки через десериализацию — не допускать десериализации экземпляров произвольных классов и тем самым не допускать прямого или косвенного выполнения их методов.
В Java 9 мы добавили фильтры десериализации (JEP 290), чтобы код приложений и библиотек мог проверять входящие потоки данных перед их десериализацией. Такой код передаёт логику проверки в виде java.io.ObjectInputFilter, когда создаёт поток десериализации (т. е. java.io.ObjectInputStream).
У подхода, при котором проверку должен явно запрашивать создатель потока, есть несколько ограничений. Этот подход плохо масштабируется, и с ним трудно обновлять фильтры после того, как код уже поставлен. Кроме того, с его помощью нельзя применить фильтрацию к операциям десериализации, которые выполняют сторонние библиотеки в приложении.
Чтобы обойти эти ограничения, JEP 290 также ввёл общий для всей JVM фильтр десериализации, который можно задать через API, системные свойства или свойства безопасности. Этот фильтр статический, поскольку задаётся ровно один раз, при запуске. Опыт использования статического общего для JVM фильтра показал, что и у него есть ограничения, особенно в сложных приложениях с несколькими слоями библиотек и несколькими контекстами выполнения. Если общий для JVM фильтр используется для каждого ObjectInputStream, он должен охватывать все контексты выполнения в приложении, поэтому в итоге такой фильтр обычно оказывается либо слишком разрешающим, либо слишком строгим.
Лучше было бы настраивать фильтры для отдельных потоков так, чтобы для этого не требовалось участие каждого создателя потока.
Чтобы защитить JVM от уязвимостей десериализации, разработчикам приложений нужно чёткое описание объектов, которые может сериализовать или десериализовать каждый компонент или библиотека. Для каждого контекста и сценария использования разработчикам следует создать и применить подходящий фильтр. Например, если приложение использует определённую библиотеку для десериализации определённой группы объектов, то при вызове этой библиотеки можно применить фильтр для соответствующих классов. Список разрешённых классов, при котором всё остальное отклоняется, защищает от объектов в потоке, которые иначе были бы неизвестными или неожиданными. Чтобы сузить набор объектов, которые разрешены или однозначно запрещены, можно использовать инкапсуляцию или другие естественные границы разделения приложения или библиотеки. Если вести список разрешённых нецелесообразно, то список запрещённых должен включать классы, пакеты и модули, которые заведомо не встречаются в потоке или заведомо вредоносны.
Лучше всех структуру и работу компонентов приложения понимает его разработчик. Это улучшение позволяет разработчику приложения создавать фильтры и применять их к каждой операции десериализации.
Описание
Как сказано выше, JEP 290 ввёл как фильтры десериализации для отдельных потоков, так и статический общий для JVM фильтр. Всякий раз при создании ObjectInputStream его фильтр для потока инициализируется статическим общим для JVM фильтром. Позже при желании этот фильтр для потока можно заменить другим фильтром.
Здесь мы вводим настраиваемую общую для JVM фабрику фильтров. Всякий раз при создании ObjectInputStream его фильтр для потока инициализируется значением, которое возвращает вызов статической общей для JVM фабрики фильтров. Таким образом, эти фильтры динамические и контекстно-зависимые, в отличие от единственного статического общего для JVM фильтра десериализации. Для обратной совместимости, если фабрика фильтров не задана, встроенная фабрика возвращает статический общий для JVM фильтр, если он был настроен.
Фабрика фильтров используется при каждой операции десериализации в среде выполнения Java, будь то код приложения, код библиотеки или код самого JDK. Фабрика специфична для приложения и должна учитывать каждый контекст выполнения десериализации в приложении. Фабрика фильтров вызывается из конструктора ObjectInputStream, а также из ObjectInputStream.setObjectInputFilter. Её аргументы — текущий фильтр и новый фильтр. При вызове из конструктора текущий фильтр равен null, а новый фильтр — это статический общий для JVM фильтр. Фабрика определяет и возвращает начальный фильтр для потока. Фабрика может создать составной фильтр с другими контекстно-зависимыми проверками или просто вернуть статический общий для JVM фильтр. Если вызывается ObjectInputStream.setObjectInputFilter, фабрика вызывается второй раз с фильтром, возвращённым при первом вызове, и запрошенным новым фильтром. Фабрика определяет, как объединить эти два фильтра, и возвращает фильтр, который заменяет фильтр потока.
В простых случаях фабрика фильтров может возвращать один фиксированный фильтр для всего приложения. Например, вот фильтр, который разрешает классы примера, разрешает классы из модуля java.base и отклоняет все остальные классы:
var filter = ObjectInputFilter.Config.createFilter("example.*;java.base/*;!*")
В приложении с несколькими контекстами выполнения фабрика фильтров может лучше защитить отдельные контексты, предоставляя для каждого из них собственный фильтр. При создании потока фабрика фильтров может определить контекст выполнения по текущему thread-local состоянию, иерархии вызывающих, библиотеке, модулю и загрузчику классов. После этого политика создания или выбора фильтров может выбрать конкретный фильтр или композицию фильтров в зависимости от контекста.
Если фильтров несколько, их результаты можно объединить. Полезный способ объединения фильтров: отклонять десериализацию, если её отклоняет любой из фильтров, разрешать, если её разрешает любой фильтр, а иначе оставлять решение неопределённым.
Использование в командной строке
Свойства jdk.serialFilter и jdk.serialFilterFactory можно задать в
командной строке, чтобы установить фильтр и фабрику фильтров. Существующее свойство jdk.serialFilter задаёт фильтр на основе шаблона.
Свойство jdk.serialFilterFactory — это имя класса фабрики фильтров, которая устанавливается перед первой десериализацией. Класс должен быть public и доступен загрузчику классов приложения.
Для совместимости с JEP 290, если свойство jdk.serialFilterFactory не задано, в качестве фабрики фильтров устанавливается встроенная фабрика, которая обеспечивает совместимость с более ранними версиями.
API
Мы определяем в классе ObjectInputFilter.Config два метода для установки и получения общей для JVM фабрики фильтров. Фабрика фильтров — это функция с двумя аргументами, текущим фильтром и следующим фильтром, которая возвращает фильтр.
/**
* Return the JVM-wide deserialization filter factory.
*
* @return the JVM-wide serialization filter factory; non-null
*/
public static BinaryOperator<ObjectInputFilter> getSerialFilterFactory();
/**
* Set the JVM-wide deserialization filter factory.
*
* The filter factory is a function of two parameters, the current filter
* and the next filter, that returns the filter to be used for the stream.
*
* @param filterFactory the serialization filter factory to set as the
* JVM-wide filter factory; not null
*/
public static void setSerialFilterFactory(BinaryOperator<ObjectInputFilter> filterFactory);
Пример
Этот класс показывает, как применить фильтр к каждой операции десериализации, которая выполняется в текущем потоке. Он определяет thread-local переменную для хранения фильтра потока, определяет фабрику фильтров, которая возвращает этот фильтр, настраивает эту фабрику как общую для JVM фабрику фильтров и предоставляет вспомогательную функцию для запуска Runnable в контексте конкретного фильтра потока.
public class FilterInThread implements BinaryOperator<ObjectInputFilter> {
// ThreadLocal to hold the serial filter to be applied
private final ThreadLocal<ObjectInputFilter> filterThreadLocal = new ThreadLocal<>();
// Construct a FilterInThread deserialization filter factory.
public FilterInThread() {}
/**
* The filter factory, which is invoked every time a new ObjectInputStream
* is created. If a per-stream filter is already set then it returns a
* filter that combines the results of invoking each filter.
*
* @param curr the current filter on the stream
* @param next a per stream filter
* @return the selected filter
*/
public ObjectInputFilter apply(ObjectInputFilter curr, ObjectInputFilter next) {
if (curr == null) {
// Called from the OIS constructor or perhaps OIS.setObjectInputFilter with no current filter
var filter = filterThreadLocal.get();
if (filter != null) {
// Prepend a filter to assert that all classes have been Allowed or Rejected
filter = ObjectInputFilter.rejectUndecidedClass(filter);
}
if (next != null) {
// Prepend the next filter to the thread filter, if any
// Initially this is the static JVM-wide filter passed from the OIS constructor
// Append the filter to reject all UNDECIDED results
filter = ObjectInputFilter.merge(next, filter);
filter = ObjectInputFilter.rejectUndecidedClass(filter);
}
return filter;
} else {
// Called from OIS.setObjectInputFilter with a current filter and a stream-specific filter.
// The curr filter already incorporates the thread filter and static JVM-wide filter
// and rejection of undecided classes
// If there is a stream-specific filter prepend it and a filter to recheck for undecided
if (next != null) {
next = ObjectInputFilter.merge(next, curr);
next = ObjectInputFilter.rejectUndecidedClass(next);
return next;
}
return curr;
}
}
/**
* Apply the filter and invoke the runnable.
*
* @param filter the serial filter to apply to every deserialization in the thread
* @param runnable a Runnable to invoke
*/
public void doWithSerialFilter(ObjectInputFilter filter, Runnable runnable) {
var prevFilter = filterThreadLocal.get();
try {
filterThreadLocal.set(filter);
runnable.run();
} finally {
filterThreadLocal.set(prevFilter);
}
}
}
Если фильтр для конкретного потока уже был задан с помощью ObjectInputStream::setObjectFilter, то фабрика фильтров объединяет этот фильтр со следующим фильтром. Если любой из фильтров отклоняет класс, этот класс отклоняется. Если любой из фильтров разрешает класс, этот класс разрешается. Иначе результат остаётся неопределённым.
Вот простой пример использования класса FilterInThread:
// Create a FilterInThread filter factory and set
var filterInThread = new FilterInThread();
ObjectInputFilter.Config.setSerialFilterFactory(filterInThread);
// Create a filter to allow example.* classes and reject all others
var filter = ObjectInputFilter.Config.createFilter("example.*;java.base/*;!*");
filterInThread.doWithSerialFilter(filter, () -> {
byte[] bytes = ...;
var o = deserializeObject(bytes);
});
Альтернативы
JEP 290 позволяет реализовывать фильтры в виде классов Java, благодаря чему возможны сложная логика и учёт контекста. Контекстно-зависимые фильтры для отдельных потоков можно было бы реализовать с помощью делегирующего фильтра, установленного на каждый поток. Чтобы определить фильтр для конкретного потока, ему нужно было бы проанализировать вызывающий код, сопоставить его с конкретным фильтром, а затем делегировать этому фильтру. Однако и сложность кода, и накладные расходы на определение вызывающего кода сказывались бы на производительности при каждом вызове.