JavaRush /Курси /JAVA 25 SELF /Безпека серіалізації: найкращі практики

Безпека серіалізації: найкращі практики

JAVA 25 SELF
Рівень 43 , Лекція 4
Відкрита

1. Основні найкращі практики для безпечної серіалізації

Серіалізація — це як пакування багажу в аеропорту: якщо ви не знаєте, що всередині, і кому довіряєте валізу, можна отримати неприємний сюрприз на контролі безпеки. У 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, механізм серіалізації JVM згенерує його автоматично на основі структури класу. Це може призвести до неочікуваних невідповідностей і, теоретично, до зловживань під час підміни класів із тим самим ім’ям, але іншою структурою.

Перевіряйте типи об’єктів під час десеріалізації

Не довіряйте тому, що надійшло мережею або з файла. Після десеріалізації завжди перевіряйте, що отриманий об’єкт має очікуваний тип, перш ніж працювати з ним.

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, а статичний ініціалізатор виконається під час завантаження класу — це вже реальна атака.

3. Типові помилки під час забезпечення безпеки серіалізації

Помилка № 1: Десеріалізація без фільтрації та перевірки типу. Часто розробники читають об’єкт із потоку й одразу приводять до потрібного типу. Це відчиняє двері для атак. Використовуйте ObjectInputFilter і перевіряйте тип через instanceof.

Помилка № 2: Зберігання чутливих даних без transient. Якщо забути оголосити паролі/ключі як transient, вони потраплять у потік і можуть витекти разом із файлом.

Помилка № 3: Відсутність serialVersionUID. Без явного serialVersionUID можливі неочікувані помилки сумісності та ризики, пов’язані з підміною класів.

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

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

1
Опитування
Налаштування серіалізації, рівень 43, лекція 4
Недоступний
Налаштування серіалізації
Налаштування серіалізації
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ