JavaRush /Курсы /JAVA 25 SELF /Безопасность сериализации: best practices

Безопасность сериализации: best practices

JAVA 25 SELF
43 уровень , 4 лекция
Открыта

1. Основные best practices для безопасной сериализации

Сериализация — это как упаковка багажа в аэропорту: если вы не знаете, что внутри, и кому доверяете чемодан, можно получить неприятный сюрприз на контроле безопасности. В Java сериализация позволяет легко сохранять и восстанавливать объекты, но при этом открывает ворота для целого спектра атак, если данные поступают из ненадёжных источников.

Классическая угроза:

Сериализация в Java может быть небезопасной. Если злоумышленник подсунет вредоносный поток, то при десериализации возможны самые неприятные последствия: от изменения полей до выполнения нежелательного кода. Это не страшилка из учебника — в истории Java действительно были случаи, когда атаки строились именно на этом механизме.

Почему так происходит?

Дело в том, что десериализация — это не просто восстановление значений полей. В процессе создаётся полноценный объект: могут вызываться специальные методы (например, readObject, readResolve), а иногда — и уязвимые места в коде через рефлексию. Особенно опасны классы из сторонних библиотек: некоторые из них выполняют действия уже на этапе десериализации. Поэтому никогда не доверяйте сериализованным данным, полученным извне.

Используйте transient для чувствительных данных

Если в вашем классе есть поля, которые содержат пароли, токены, приватные ключи или другую чувствительную информацию, объявляйте их как transient. Эти данные не попадут в сериализованный поток.

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private transient String password; // не сериализуется

    // ...конструкторы, геттеры, сеттеры...
}

Что произойдёт при десериализации? Поле password будет иметь значение по умолчанию (null для строк). Это хорошо: пароли не будут храниться в файлах/пересылаться по сети.

Явно определяйте serialVersionUID

Всегда явно указывайте serialVersionUID. Это снижает вероятность ошибок совместимости и минимизирует риск подмены классов при десериализации.

private static final long serialVersionUID = 1L;

Почему это важно для безопасности? Если не указать serialVersionUID, компилятор сгенерирует его автоматически на основе структуры класса. Это может привести к неожиданным несовпадениям и, в теории, к злоупотреблениям при подмене классов с тем же именем, но другой структурой.

Проверяйте типы объектов при десериализации

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

Object obj = objectInputStream.readObject();
if (obj instanceof User) {
    User user = (User) obj;
    // безопасно работаем с user
} else {
    // неожиданный тип — выбросить исключение или обработать ошибку
}

Зачем это нужно? Вредоносный поток может содержать объект другого класса, который реализует Serializable, но не соответствует вашей бизнес-логике.

Ограничивайте классы, которые можно десериализовать (ObjectInputFilter)

Начиная с Java 9 используйте фильтры — ObjectInputFilter, чтобы ограничить набор классов, которые разрешено десериализовать. Это как фейс-контроль на входе.

Пример: установка фильтра

import java.io.*;

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.example.User;com.example.Address;!*"
);

ObjectInputStream in = new ObjectInputStream(inputStream);
in.setObjectInputFilter(filter);

Object obj = in.readObject(); // теперь десериализуются только User и Address

Этот фильтр разрешает только классы User и Address вашего приложения. Все остальные будут заблокированы — выбросится исключение. Это существенно снижает риск попадания вредоносного объекта.

Не десериализуйте данные из ненадёжных источников

Золотое правило: если вы не уверены в источнике данных — не десериализуйте. Предпочитайте форматы, которые не выполняют код при разборе (например, JSON, XML с безопасными парсерами).

Пример плохой практики:

// Никогда не делайте так с данными из интернета!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // опасно!

Что делать лучше?

  • Использовать JSON-парсеры (например, Gson/Jackson) или XML-парсеры с валидацией.
  • Если бинарная сериализация необходима — фильтруйте классы через ObjectInputFilter и проверяйте типы (instanceof).

Используйте альтернативные форматы для обмена с внешними системами

Для интеграций используйте форматы, которые не исполняют код при разборе: JSON, XML, Protocol Buffers и др. Это почти исключает атаки через десериализацию.

// Вместо ObjectInputStream используйте JSON-парсер
User user = gson.fromJson(jsonString, User.class);

Не храните сериализованные объекты в общедоступных местах

Файлы с сериализованными объектами могут содержать чувствительные данные. Не храните их в общедоступных каталогах и ограничивайте права доступа на уровне файловой системы.

Не полагайтесь на сериализацию для контроля целостности

Сериализация не гарантирует целостность или подлинность данных. Используйте цифровые подписи, контрольные суммы или шифрование, если изменения недопустимы.

2. Практика: пример ObjectInputFilter и демонстрация уязвимости

Пример фильтрации классов

Допустим, у нас есть класс User:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password;

    // ...конструкторы, геттеры, сеттеры...
}

Фильтр разрешает только User:

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.example.User;!*"
);
in.setObjectInputFilter(filter);

Теперь, если кто-то попробует подсунуть объект другого класса, десериализация завершится с ошибкой.

Демонстрация потенциальной уязвимости

Вредоносный класс:

// Представим, что кто-то подсунул такой класс
public class Evil implements java.io.Serializable {
    static {
        System.out.println("Вредоносный код выполнен!");
        // здесь может быть что угодно...
    }
}

Если не фильтровать классы, при десериализации может быть создан объект Evil, а статический инициализатор выполнится при загрузке класса — это уже реальная атака.

4. Типичные ошибки при обеспечении безопасности сериализации

Ошибка №1: Десериализация без фильтрации и проверки типа. Часто разработчики читают объект из потока и сразу кастуют к нужному типу. Это открывает дверь для атак. Используйте ObjectInputFilter и проверяйте тип через instanceof.

Ошибка №2: Хранение чувствительных данных без transient. Если забыть объявить пароли/ключи как transient, они попадут в поток и могут утечь вместе с файлом.

Ошибка №3: Отсутствие serialVersionUID. Без явного serialVersionUID возможны неожиданные ошибки совместимости и риски, связанные с подменой классов.

Ошибка №4: Использование сериализации для обмена с внешними системами. Бинарная сериализация удобна внутри приложения (например, кэш), но опасна для внешнего обмена. Предпочитайте JSON/XML/Proto с безопасными парсерами.

Ошибка №5: Игнорирование целостности данных. Изменение байтов сериализованного файла останется незамеченным. Применяйте цифровые подписи, контрольные суммы или шифрование.

1
Задача
JAVA 25 SELF, 43 уровень, 4 лекция
Недоступна
Артефакты Чародея: Метка Версии для Сокровищ
Артефакты Чародея: Метка Версии для Сокровищ
1
Задача
JAVA 25 SELF, 43 уровень, 4 лекция
Недоступна
Клиенты CRM: Гарантия Целостности Данных При Загрузке
Клиенты CRM: Гарантия Целостности Данных При Загрузке
1
Опрос
Настройка сериализации, 43 уровень, 4 лекция
Недоступен
Настройка сериализации
Настройка сериализации
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ