J2EE Bad Practices: Nicht-serialisierbares Objekt in Session gespeichert
Beschreibung
J2EE Bad Practices: Nicht-serialisierbares Objekt in Session gespeichert ist eine Schwachstelle, bei der eine Java-Webanwendung Objekte, die das Serializable-Interface nicht implementieren, als HttpSession-Attribute speichert. In J2EE-Umgebungen replizieren Container häufig HttpSession-Objekte über mehrere JVMs für Lastverteilung und Failover-Zwecke. Diese Replikation erfordert, dass alle Session-Attribute serialisierbar sind, damit sie zwischen Servern übertragen werden können. Wenn nicht-serialisierbare Objekte in Sessions gespeichert werden, schlägt die Replikation fehl, was Session-Datenverlust bei Failover, Lastverteilungsfehler und unvorhersehbares Anwendungsverhalten verursacht.
Risiko
Das Speichern nicht-serialisierbarer Objekte in Sessions erzeugt erhebliche Zuverlässigkeits- und Verfügbarkeitsrisiken. Während eines Server-Failovers verlieren Benutzer ihren Session-Zustand, was Neuauthentifizierung und Verlust laufender Arbeit erzwingt. Lastverteiler können Anfragen nicht frei über Server verteilen, was die Skalierbarkeit reduziert. Session-Persistenz auf Festplatte schlägt fehl, was bedeutet, dass Server-Neustarts alle Sessions verlieren. Die Anwendung kann in der Entwicklung mit einem einzelnen Server zu funktionieren scheinen, aber in Produktions-Clustern katastrophal fehlschlagen. Einige Container werfen Exceptions beim Versuch, nicht-serialisierbare Sessions zu replizieren, was Anfragefehler verursacht. Benutzer erleben inkonsistentes Verhalten abhängig davon, welcher Server ihre Anfragen behandelt.
Lösung
Stellen Sie sicher, dass alle in HttpSession gespeicherten Objekte das Serializable-Interface implementieren. Für komplexe Objekte implementieren Sie ordnungsgemäße Serialisierung mit serialVersionUID-Feldern. Vermeiden Sie das Speichern nicht-serialisierbarer Ressourcen wie Datenbankverbindungen, Threads oder I/O-Streams in Sessions; speichern Sie stattdessen Identifikatoren und erstellen Sie Ressourcen bei Bedarf neu. Verwenden Sie das transient-Schlüsselwort für Felder, die nicht serialisiert werden können oder sollen. Erwägen Sie die Verwendung von Wrapper-Klassen, die Serializable für Drittanbieter-Objekte implementieren. Testen Sie Session-Replikation in der Entwicklung mit geclusterten Konfigurationen. Verwenden Sie statische Analysewerkzeuge, um nicht-serialisierbare Session-Attribute zu erkennen.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Verfügbarkeit | Bereich: Verfügbarkeit DoS: Absturz, Beenden oder Neustart - Session-Replikationsfehler können Anfragefehler und Dienstunterbrechungen während Failover-Ereignissen verursachen. |
| Sonstige | Bereich: Sonstige Qualitätsverschlechterung - Anwendungen werden in geclusterten Deployments unzuverlässig und schlagen bei Failover oder Lastverteilung fehl. |
Beispielcode
Verwundbarer Code
// Verwundbar: Nicht-serialisierbare Klasse in Session gespeichert
public class UserPreferences {
// Fehlt: implements Serializable
private String theme;
private String language;
private List<String> recentItems;
public UserPreferences(String theme, String language) {
this.theme = theme;
this.language = language;
this.recentItems = new ArrayList<>();
}
// Getter und Setter...
}
// Verwundbar: Servlet speichert nicht-serialisierbares Objekt
@WebServlet("/login")
public class VulnerableLoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
if (authenticate(username, password)) {
HttpSession session = request.getSession();
// Verwundbar: UserPreferences ist nicht Serializable
UserPreferences prefs = loadUserPreferences(username);
session.setAttribute("userPrefs", prefs); // Replikation schlägt fehl!
// Verwundbar: Nicht-serialisierbare Datenbankverbindung speichern
Connection conn = getConnection();
session.setAttribute("dbConnection", conn); // Kann nicht serialisiert werden!
// Verwundbar: Thread-Referenz speichern
Thread backgroundTask = new Thread(() -> doBackgroundWork());
session.setAttribute("task", backgroundTask); // Nicht serialisierbar!
response.sendRedirect("/dashboard");
}
}
}
// Verwundbar: Klasse mit nicht-serialisierbaren Feldern
public class ShoppingCart implements Serializable {
private static final long serialVersionUID = 1L;
private List<CartItem> items; // OK wenn CartItem Serializable ist
private Connection dbConnection; // Verwundbar: Connection nicht serialisierbar
private Logger logger; // Verwundbar: Logger oft nicht serialisierbar
private Thread priceUpdateThread; // Verwundbar: Thread nicht serialisierbar
public ShoppingCart() {
this.items = new ArrayList<>();
this.dbConnection = getConnection(); // Schlägt bei Serialisierung fehl
this.logger = LoggerFactory.getLogger(ShoppingCart.class);
}
}
// Verwundbar: Komplexe Objektgraphen mit nicht-serialisierbaren Mitgliedern speichern
public class UserSession {
// Fehlt: implements Serializable
private User user;
private Map<String, Object> attributes; // Object könnte nicht-serialisierbar sein
private InputStream uploadStream; // Nicht serialisierbar
private Socket socket; // Nicht serialisierbar
public void setAttribute(String key, Object value) {
// Verwundbar: Keine Prüfung ob value serialisierbar ist
attributes.put(key, value);
}
}
// Verwundbar: JSF Managed Bean nicht serialisierbar
@Named
@SessionScoped
public class VulnerableUserBean {
// Fehlt: implements Serializable
private String username;
private List<Message> messages;
private Connection connection; // Nicht-serialisierbares Feld
// Session-Scoped aber nicht serialisierbar - Replikation schlägt fehl
}
Lösungscode
// Behoben: Serialisierbares Session-Objekt
import java.io.Serializable;
public class UserPreferences implements Serializable {
private static final long serialVersionUID = 1L;
private String theme;
private String language;
private List<String> recentItems; // ArrayList ist Serializable
public UserPreferences(String theme, String language) {
this.theme = theme;
this.language = language;
this.recentItems = new ArrayList<>();
}
// Getter und Setter...
}
// Behoben: Servlet speichert nur serialisierbare Objekte
@WebServlet("/login")
public class SecureLoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
if (authenticate(username, password)) {
HttpSession session = request.getSession();
// Behoben: UserPreferences implementiert jetzt Serializable
UserPreferences prefs = loadUserPreferences(username);
session.setAttribute("userPrefs", prefs);
// Behoben: Benutzer-ID statt Verbindung speichern
session.setAttribute("userId", getUserId(username));
// Behoben: Task-ID speichern, nicht Thread-Referenz
String taskId = startBackgroundTask(username);
session.setAttribute("backgroundTaskId", taskId);
response.sendRedirect("/dashboard");
}
}
}
// Behoben: Ordnungsgemäß serialisierbare Klasse mit transienten Feldern
public class ShoppingCart implements Serializable {
private static final long serialVersionUID = 1L;
private List<CartItem> items; // CartItem muss Serializable sein
// Behoben: Nicht-serialisierbare Felder als transient markieren
private transient Connection dbConnection;
private transient Logger logger;
// Behoben: Thread nicht speichern - Task-Management verwenden
private String priceUpdateTaskId;
public ShoppingCart() {
this.items = new ArrayList<>();
}
// Behoben: Transiente Felder nach Deserialisierung reinitialisieren
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Transiente Felder reinitialisieren
this.logger = LoggerFactory.getLogger(ShoppingCart.class);
// dbConnection sollte bei Bedarf geholt werden, nicht gespeichert
}
// Behoben: Verbindung bei Bedarf holen, nicht speichern
private Connection getConnection() {
// Jedes Mal aus Pool holen
return DataSourceProvider.getConnection();
}
public void addItem(CartItem item) {
items.add(item);
// Frische Verbindung verwenden
try (Connection conn = getConnection()) {
saveToDatabase(conn, item);
} catch (SQLException e) {
logger.error("Speichern des Warenkorb-Artikels fehlgeschlagen", e);
}
}
}
// Behoben: Serialisierbarer Warenkorb-Artikel
public class CartItem implements Serializable {
private static final long serialVersionUID = 1L;
private String productId;
private String productName;
private int quantity;
private BigDecimal price; // BigDecimal ist Serializable
// Alle Felder sind serialisierbare Primitive oder serialisierbare Objekte
}
// Behoben: Session-Klasse mit ordnungsgemäßer Serialisierung
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private User user; // User muss Serializable implementieren
private Map<String, Serializable> attributes; // Nur Serializable-Werte
// Behoben: Referenz-ID statt nicht-serialisierbarer Ressource speichern
private String uploadId; // Referenz auf Upload, nicht Stream
private transient InputStream uploadStream;
public UserSession() {
this.attributes = new HashMap<>();
}
// Behoben: Typsichere Methode die Serialisierbarkeit sicherstellt
public void setAttribute(String key, Serializable value) {
attributes.put(key, value);
}
// Behoben: Generische Methode mit Laufzeitprüfung
public void setAttributeChecked(String key, Object value) {
if (value != null && !(value instanceof Serializable)) {
throw new IllegalArgumentException(
"Session-Attribut muss Serializable sein: " + key);
}
attributes.put(key, (Serializable) value);
}
}
// Behoben: User-Klasse implementiert Serializable
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
private Set<String> roles; // HashSet ist Serializable
private Date lastLogin; // Date ist Serializable
// Passwort-Hash nicht in Session speichern
// transient private String passwordHash;
}
// Behoben: JSF Managed Bean mit ordnungsgemäßer Serialisierung
@Named
@SessionScoped
public class SecureUserBean implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private List<Message> messages; // Message muss Serializable sein
// Behoben: Keine Verbindung speichern - injizierte DataSource verwenden
@Inject
private transient DataSource dataSource;
// Nach Deserialisierung reinitialisieren
@PostConstruct
public void init() {
// Initialisieren oder Daten laden
}
}
// Behoben: Wrapper für nicht-serialisierbare Drittanbieter-Objekte
public class SerializableWrapper<T> implements Serializable {
private static final long serialVersionUID = 1L;
private transient T wrapped;
private byte[] serializedData;
private Class<T> wrappedClass;
public SerializableWrapper(T object, Serializer<T> serializer) {
this.wrapped = object;
this.wrappedClass = (Class<T>) object.getClass();
this.serializedData = serializer.serialize(object);
}
public T get(Serializer<T> serializer) {
if (wrapped == null && serializedData != null) {
wrapped = serializer.deserialize(serializedData, wrappedClass);
}
return wrapped;
}
}
// Behoben: Session-Listener zur Validierung der Serialisierung
@WebListener
public class SessionSerializationListener implements HttpSessionAttributeListener {
private static final Logger logger =
LoggerFactory.getLogger(SessionSerializationListener.class);
@Override
public void attributeAdded(HttpSessionBindingEvent event) {
validateSerializable(event);
}
@Override
public void attributeReplaced(HttpSessionBindingEvent event) {
validateSerializable(event);
}
private void validateSerializable(HttpSessionBindingEvent event) {
Object value = event.getValue();
if (value != null && !(value instanceof Serializable)) {
logger.warn("Nicht-serialisierbares Objekt in Session gespeichert: " +
event.getName() + " = " + value.getClass().getName());
// In der Entwicklung möchten Sie vielleicht eine Exception werfen
// throw new IllegalArgumentException(
// "Session-Attribut muss Serializable sein");
}
}
}
CVE-Beispiele
Keine spezifischen CVEs werden dieser CWE üblicherweise zugeordnet, da sie primär die Anwendungszuverlässigkeit betrifft statt Sicherheitsschwachstellen.
Referenzen
- MITRE Corporation. "CWE-579: J2EE Bad Practices: Non-serializable Object Stored in Session." https://cwe.mitre.org/data/definitions/579.html
- Oracle. "Java Servlet Specification - HttpSession."
- Jakarta EE. "Jakarta Servlet Specification."