J2EE Framework: Nicht-serialisierbare Objekte auf Festplatte speichern

Beschreibung

J2EE Framework: Nicht-serialisierbare Objekte auf Festplatte speichern tritt auf, wenn eine J2EE-Anwendung versucht, Session-Daten oder andere Objekte zu persistieren, die das Serializable-Interface nicht implementieren. Wenn der Application Server Sessions passivieren, über einen Cluster replizieren oder für Failover persistieren muss, verursachen nicht-serialisierbare Objekte Fehler. Die Anwendung kann abstürzen, Session-Daten verlieren oder nicht ordnungsgemäß skalieren.

Risiko

Session-Datenverlust kann dazu führen, dass Benutzer ihre Arbeit verlieren oder unerwartet abgemeldet werden. In geclusterten Umgebungen können Sessions nicht repliziert werden, was Hochverfügbarkeit bricht. Application Server können Exceptions während der Passivierung werfen und potenziell die Anwendung zum Absturz bringen. Sicherheitskontexte, die in Sessions gespeichert sind, persistieren möglicherweise nicht korrekt, was zu Authentifizierungsproblemen führt. Die Anwendung kann in der Entwicklung zu funktionieren scheinen, aber in der Produktion fehlschlagen.

Lösung

Stellen Sie sicher, dass alle Objekte, die in HTTP-Sessions gespeichert werden, Serializable implementieren. Markieren Sie Felder, die nicht serialisiert werden sollen, als transient. Implementieren Sie benutzerdefinierte Serialisierung für komplexe Objekte. Verwenden Sie Serialisierungs-Proxies für sicherheitssensitive Klassen. Testen Sie Session-Serialisierung in der Entwicklung. Erwägen Sie alternative Session-Speichermechanismen, die keine Java-Serialisierung erfordern.

Häufige Auswirkungen

AuswirkungDetails
VerfügbarkeitBereich: Session-Verlust

Sessions können nicht persistiert oder repliziert werden, was Datenverlust verursacht.
ZuverlässigkeitBereich: Clustering-Fehler

Anwendung kann nicht über mehrere Server skalieren.
SicherheitBereich: Authentifizierungsprobleme

Sicherheitskontext persistiert möglicherweise nicht korrekt.

Beispielcode + Lösungscode

Verwundbarer Code

// VERWUNDBAR: Nicht-serialisierbares Objekt in Session
public class UserPreferences {
    // Fehlt: implements Serializable
    private String theme;
    private String language;
    private Connection dbConnection;  // Sowieso nicht serialisierbar

    // Getter und Setter
}

@WebServlet("/preferences")
public class VulnerablePreferencesServlet extends HttpServlet {

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response) {
        HttpSession session = request.getSession();

        UserPreferences prefs = new UserPreferences();
        prefs.setTheme(request.getParameter("theme"));

        // FEHLER: UserPreferences ist nicht Serializable!
        session.setAttribute("userPrefs", prefs);
        // Wird fehlschlagen wenn Session passiviert wird
    }
}

// VERWUNDBAR: Session-Bean mit nicht-serialisierbarem Feld
public class ShoppingCart implements Serializable {
    private static final long serialVersionUID = 1L;

    private List<CartItem> items;
    private DatabaseHelper dbHelper;  // Nicht serialisierbar!

    public void addItem(CartItem item) {
        items.add(item);
        dbHelper.logPurchase(item);  // Verwendet nicht-serialisierbares Feld
    }
}

// VERWUNDBAR: Anonyme innere Klasse (oft nicht serialisierbar)
@WebServlet("/process")
public class VulnerableProcessServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) {
        HttpSession session = request.getSession();

        // Anonyme Klasse erfasst äußere Klassenreferenz
        Runnable task = new Runnable() {
            @Override
            public void run() {
                // Etwas verarbeiten
            }
        };

        // Anonyme innere Klassen sind möglicherweise nicht serialisierbar
        session.setAttribute("pendingTask", task);
    }
}

// VERWUNDBAR: Lambda erfasst nicht-serialisierbaren Zustand
public class VulnerableService {
    private DataSource dataSource;  // Nicht serialisierbar

    public void processRequest(HttpServletRequest request) {
        HttpSession session = request.getSession();

        // Lambda erfasst nicht-serialisierbares Feld
        Consumer<String> processor = (data) -> {
            try (Connection conn = dataSource.getConnection()) {
                // Verbindung verwenden
            } catch (SQLException e) {
                // Behandeln
            }
        };

        // Wird Serialisierung fehlschlagen
        session.setAttribute("processor", processor);
    }
}

// VERWUNDBAR: Entity mit lazy-geladenen Collections
@Entity
public class VulnerableUser implements Serializable {
    @Id
    private Long id;

    @OneToMany(fetch = FetchType.LAZY)
    private List<Order> orders;  // Hibernate-Proxy - nicht serialisierbar!

    // Wenn Session serialisiert wird, verursacht Proxy Probleme
}
// VERWUNDBAR: EJB mit nicht-serialisierbarem Zustand
@Stateful
@SessionScoped
public class VulnerableCartBean {

    private List<Item> items = new ArrayList<>();
    private Logger logger = LoggerFactory.getLogger(getClass());  // Nicht serialisierbar!

    public void addItem(Item item) {
        items.add(item);
        logger.info("Artikel hinzugefügt: {}", item);
    }

    // Stateful Bean kann nicht passiviert werden
}

// VERWUNDBAR: CDI-Bean im Session-Scope
@SessionScoped
@Named
public class VulnerableSessionBean {
    // Serializable-Interface fehlt!

    private UserData userData;
    private transient Service service;  // Transient aber Bean selbst nicht serialisierbar

    public void init() {
        userData = loadUserData();
    }
}

Lösungscode

// SICHER: Serialisierbares Session-Objekt
public class UserPreferences implements Serializable {
    private static final long serialVersionUID = 1L;

    private String theme;
    private String language;

    // Nicht-serialisierbare Ressourcen als transient markiert
    private transient Connection dbConnection;

    // Getter und Setter
    public String getTheme() { return theme; }
    public void setTheme(String theme) { this.theme = theme; }
    public String getLanguage() { return language; }
    public void setLanguage(String language) { this.language = language; }

    // Benutzerdefinierte Serialisierung für transiente Felder
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
    }

    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        // Transiente Felder nach Deserialisierung reinitialisieren
        this.dbConnection = null;  // Wird lazy initialisiert
    }
}

@WebServlet("/preferences")
public class SafePreferencesServlet extends HttpServlet {

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response) {
        HttpSession session = request.getSession();

        UserPreferences prefs = new UserPreferences();
        prefs.setTheme(request.getParameter("theme"));

        // Sicher: UserPreferences implementiert Serializable
        session.setAttribute("userPrefs", prefs);
    }
}

// SICHER: Warenkorb mit ordnungsgemäßer Serialisierung
public class ShoppingCart implements Serializable {
    private static final long serialVersionUID = 1L;

    private List<CartItem> items = new ArrayList<>();

    // Nicht-serialisierbare Felder als transient markieren
    private transient DatabaseHelper dbHelper;

    public void addItem(CartItem item) {
        items.add(item);
        getDbHelper().logPurchase(item);
    }

    // Lazy-Initialisierung nach Deserialisierung
    private DatabaseHelper getDbHelper() {
        if (dbHelper == null) {
            dbHelper = new DatabaseHelper();
        }
        return dbHelper;
    }

    // Benutzerdefinierte Serialisierung
    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        // Transiente Felder reinitialisieren
        this.dbHelper = null;
    }
}

// SICHER: Serialisierbarer Task-Wrapper
public class SerializableTask implements Serializable, Runnable {
    private static final long serialVersionUID = 1L;

    private String taskData;

    public SerializableTask(String data) {
        this.taskData = data;
    }

    @Override
    public void run() {
        // Mit taskData verarbeiten
    }
}

@WebServlet("/process")
public class SafeProcessServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) {
        HttpSession session = request.getSession();

        // Serialisierbare Task-Klasse statt anonymer Klasse verwenden
        SerializableTask task = new SerializableTask("data");
        session.setAttribute("pendingTask", task);
    }
}

// SICHER: DTO-Pattern für Entities
public class UserDTO implements Serializable {
    private static final long serialVersionUID = 1L;

    private Long id;
    private String name;
    private List<OrderDTO> orders;  // DTOs, nicht Entities

    // Von Entity konvertieren (vom Persistenzkontext trennen)
    public static UserDTO fromEntity(User user) {
        UserDTO dto = new UserDTO();
        dto.id = user.getId();
        dto.name = user.getName();

        // Verwandte Entities eifrig laden und konvertieren
        dto.orders = user.getOrders().stream()
            .map(OrderDTO::fromEntity)
            .collect(Collectors.toList());

        return dto;
    }

    // Getter und Setter
}

// Verwendung in Servlet
public void storeUserInSession(HttpServletRequest request, User user) {
    HttpSession session = request.getSession();

    // Entity zu DTO konvertieren vor Speicherung in Session
    UserDTO userDto = UserDTO.fromEntity(user);
    session.setAttribute("user", userDto);
}
// SICHER: Stateful EJB mit ordnungsgemäßer Serialisierung
@Stateful
@SessionScoped
public class SafeCartBean implements Serializable {
    private static final long serialVersionUID = 1L;

    private List<Item> items = new ArrayList<>();

    // Logger ist transient
    private transient Logger logger;

    private Logger getLogger() {
        if (logger == null) {
            logger = LoggerFactory.getLogger(getClass());
        }
        return logger;
    }

    public void addItem(Item item) {
        items.add(item);
        getLogger().info("Artikel hinzugefügt: {}", item);
    }

    @PrePassivate
    public void prePassivate() {
        // Vor Passivierung aufräumen
        logger = null;
    }

    @PostActivate
    public void postActivate() {
        // Nach Aktivierung reinitialisieren
        logger = LoggerFactory.getLogger(getClass());
    }
}

// SICHER: CDI Session-scoped Bean
@SessionScoped
@Named
public class SafeSessionBean implements Serializable {
    private static final long serialVersionUID = 1L;

    private UserData userData;

    @Inject
    private transient UserService userService;  // Injiziert, transient

    @PostConstruct
    public void init() {
        userData = loadUserData();
    }

    private UserData loadUserData() {
        return userService.getCurrentUser();
    }
}

// SICHER: Serialisierbarkeit in Tests validieren
public class SerializabilityTest {

    @Test
    public void testUserPreferencesSerializable() throws Exception {
        UserPreferences prefs = new UserPreferences();
        prefs.setTheme("dark");
        prefs.setLanguage("en");

        // Serialisieren
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(prefs);
        oos.close();

        // Deserialisieren
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        UserPreferences restored = (UserPreferences) ois.readObject();
        ois.close();

        // Verifizieren
        assertEquals("dark", restored.getTheme());
        assertEquals("en", restored.getLanguage());
    }
}

Ausgenutzt in der Praxis

Session-Verlust in Produktion

Anwendungen, die in der Entwicklung funktionierten, scheiterten in Produktions-Clustern aufgrund von Session-Replikationsfehlern.

Failover-Fehler

Systeme könnten Benutzer-Sessions nach Server-Neustarts nicht wiederherstellen aufgrund nicht-serialisierbarer Session-Daten.

Skalierungsprobleme

Anwendungen könnten nicht horizontal skalieren, weil Sessions nicht verteilt werden könnten.


Tools zum Testen/Ausnutzen

  • JVM-Serialisierungs-Testframeworks
  • Application-Server-Logs für NotSerializableException
  • Clustering-Testumgebungen
  • Statische Analysewerkzeuge zur Erkennung von Serializable-Problemen

CVE-Beispiele

  • Session-Management-Schwachstellen durch Serialisierungsfehler
  • Hochverfügbarkeitsfehler durch nicht-serialisierbare Session-Daten

Referenzen

  1. MITRE. "CWE-594: J2EE Framework: Saving Unserializable Objects to Disk." https://cwe.mitre.org/data/definitions/594.html
  2. Java EE Best Practices für Session-Management.