CodeGym /Kursy /JAVA 25 SELF /Bezpieczeństwo serializacji: najlepsze praktyki

Bezpieczeństwo serializacji: najlepsze praktyki

JAVA 25 SELF
Poziom 43 , Lekcja 4
Dostępny

1. Podstawowe najlepsze praktyki dla bezpiecznej serializacji

Serializacja to jak pakowanie bagażu na lotnisku: jeśli nie wiesz, co jest w środku i komu powierzasz walizkę, możesz mieć niemiłą niespodziankę przy kontroli bezpieczeństwa. W Javie serializacja pozwala łatwo zapisywać i odtwarzać obiekty, ale jednocześnie otwiera furtkę do całego spektrum ataków, jeśli dane pochodzą z niezaufanych źródeł.

Klasyczne zagrożenie:

Serializacja w Javie może być niebezpieczna. Jeśli napastnik podsunie złośliwy strumień, to podczas deserializacji możliwe są bardzo przykre konsekwencje: od zmiany pól po wykonanie niepożądanego kodu. To nie straszak z podręcznika – w historii Javy rzeczywiście były przypadki ataków opartych właśnie na tym mechanizmie.

Dlaczego tak się dzieje?

Chodzi o to, że deserializacja to nie tylko odtworzenie wartości pól. W trakcie powstaje pełnoprawny obiekt: mogą zostać wywołane specjalne metody (na przykład readObject, readResolve), a czasem także podatne miejsca w kodzie przez refleksję. Szczególnie niebezpieczne są klasy z zewnętrznych bibliotek: niektóre z nich wykonują działania już na etapie deserializacji. Dlatego nigdy nie ufaj serializowanym danym pozyskanym z zewnątrz.

Używaj transient dla danych wrażliwych

Jeśli w twojej klasie są pola zawierające hasła, tokeny, klucze prywatne lub inne wrażliwe informacje, oznacz je jako transient. Te dane nie trafią do strumienia serializowanego.

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private transient String password; // nie podlega serializacji

    // ...konstruktory, gettery, settery...
}

Co się stanie przy deserializacji? Pole password będzie miało wartość domyślną (null dla String). To dobrze: hasła nie będą zapisywane w plikach ani przesyłane przez sieć.

Jawnie definiuj serialVersionUID

Zawsze jawnie określaj serialVersionUID. To zmniejsza ryzyko błędów zgodności i minimalizuje ryzyko podmiany klas podczas deserializacji.

private static final long serialVersionUID = 1L;

Dlaczego to ważne dla bezpieczeństwa? Jeśli nie podasz serialVersionUID, kompilator wygeneruje go automatycznie na podstawie struktury klasy. Może to prowadzić do nieoczekiwanych rozbieżności i, teoretycznie, do nadużyć przy podmianie klas o tej samej nazwie, lecz innej strukturze.

Sprawdzaj typy obiektów podczas deserializacji

Nie ufaj temu, co przyszło z sieci lub z pliku. Po deserializacji zawsze sprawdzaj, czy zwrócony obiekt ma oczekiwany typ, zanim zaczniesz z nim pracować.

Object obj = objectInputStream.readObject();
if (obj instanceof User) {
    User user = (User) obj;
    // bezpiecznie pracujemy z user
} else {
    // nieoczekiwany typ – rzuć wyjątek lub obsłuż błąd
}

Po co to robić? Złośliwy strumień może zawierać obiekt innej klasy, który implementuje Serializable, ale nie odpowiada twojej logice biznesowej.

Ograniczaj klasy, które można deserializować (ObjectInputFilter)

Począwszy od Java 9 używaj filtrów – ObjectInputFilter, aby ograniczyć zbiór klas dozwolonych do deserializacji. To jak selekcja przy wejściu.

Przykład: ustawienie filtra

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(); // teraz deserializowane są tylko User i Address

Ten filtr zezwala wyłącznie na klasy User i Address twojej aplikacji. Wszystkie pozostałe zostaną zablokowane – zostanie rzucony wyjątek. To znacząco zmniejsza ryzyko wstrzyknięcia złośliwego obiektu.

Nie deserializuj danych z niezaufanych źródeł

Złota zasada: jeśli nie masz pewności co do źródła danych – nie deserializuj. Preferuj formaty, które nie wykonują kodu podczas parsowania (na przykład JSON, XML z bezpiecznymi parserami).

Przykład złej praktyki:

// Nigdy tak nie rób z danymi z Internetu!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // niebezpieczne!

Co robić lepiej?

  • Używaj parserów JSON (np. Gson/Jackson) lub parserów XML z walidacją.
  • Jeśli binarna serializacja jest konieczna – filtruj klasy przez ObjectInputFilter i sprawdzaj typy (instanceof).

Używaj alternatywnych formatów do wymiany z systemami zewnętrznymi

Do integracji używaj formatów, które nie wykonują kodu podczas parsowania: JSON, XML, Protocol Buffers itd. To niemal eliminuje ataki przez deserializację.

// Zamiast ObjectInputStream użyj parsera JSON
User user = gson.fromJson(jsonString, User.class);

Nie przechowuj serializowanych obiektów w publicznie dostępnych miejscach

Pliki z serializowanymi obiektami mogą zawierać wrażliwe dane. Nie przechowuj ich w publicznie dostępnych katalogach i ograniczaj uprawnienia na poziomie systemu plików.

Nie polegaj na serializacji w kwestii integralności

Serializacja nie gwarantuje integralności ani autentyczności danych. Używaj podpisów cyfrowych, sum kontrolnych lub szyfrowania, jeśli zmiany są niedopuszczalne.

2. Praktyka: przykład ObjectInputFilter i demonstracja podatności

Przykład filtrowania klas

Załóżmy, że mamy klasę User:

import java.io.Serializable;

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

    // ...konstruktory, gettery, settery...
}

Filtr zezwala tylko na User:

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

Teraz, jeśli ktoś spróbuje podłożyć obiekt innej klasy, deserializacja zakończy się błędem.

Demonstracja potencjalnej podatności

Złośliwa klasa:

// Wyobraźmy sobie, że ktoś podłożył taką klasę
public class Evil implements java.io.Serializable {
    static {
        System.out.println("Złośliwy kod został wykonany!");
        // tutaj może być cokolwiek...
    }
}

Jeśli nie będziesz filtrować klas, podczas deserializacji może zostać utworzony obiekt Evil, a inicjalizator statyczny wykona się przy ładowaniu klasy – to już realny atak.

4. Typowe błędy przy zapewnianiu bezpieczeństwa serializacji

Błąd nr 1: Deserializacja bez filtrowania i sprawdzania typu. Często programiści odczytują obiekt ze strumienia i od razu rzutują go na potrzebny typ. To otwiera drzwi dla ataków. Używaj ObjectInputFilter i sprawdzaj typ przez instanceof.

Błąd nr 2: Przechowywanie danych wrażliwych bez transient. Jeśli zapomnisz oznaczyć hasła/klucze jako transient, trafią do strumienia i mogą wyciec wraz z plikiem.

Błąd nr 3: Brak serialVersionUID. Bez jawnego serialVersionUID możliwe są nieoczekiwane błędy zgodności i ryzyka związane z podmianą klas.

Błąd nr 4: Użycie serializacji do wymiany z systemami zewnętrznymi. Binarna serializacja jest wygodna wewnątrz aplikacji (np. cache), ale niebezpieczna dla zewnętrznej wymiany. Preferuj JSON/XML/Proto z bezpiecznymi parserami.

Błąd nr 5: Ignorowanie integralności danych. Zmiana bajtów serializowanego pliku może pozostać niezauważona. Stosuj podpisy cyfrowe, sumy kontrolne lub szyfrowanie.

1
Ankieta/quiz
Konfiguracja serializacji, poziom 43, lekcja 4
Niedostępny
Konfiguracja serializacji
Konfiguracja serializacji
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION