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
| Auswirkung | Details |
|---|---|
| Verfügbarkeit | Bereich: Session-Verlust Sessions können nicht persistiert oder repliziert werden, was Datenverlust verursacht. |
| Zuverlässigkeit | Bereich: Clustering-Fehler Anwendung kann nicht über mehrere Server skalieren. |
| Sicherheit | Bereich: 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
- MITRE. "CWE-594: J2EE Framework: Saving Unserializable Objects to Disk." https://cwe.mitre.org/data/definitions/594.html
- Java EE Best Practices für Session-Management.