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

AuswirkungDetails
VerfügbarkeitBereich: Verfügbarkeit

DoS: Absturz, Beenden oder Neustart - Session-Replikationsfehler können Anfragefehler und Dienstunterbrechungen während Failover-Ereignissen verursachen.
SonstigeBereich: 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

  1. MITRE Corporation. "CWE-579: J2EE Bad Practices: Non-serializable Object Stored in Session." https://cwe.mitre.org/data/definitions/579.html
  2. Oracle. "Java Servlet Specification - HttpSession."
  3. Jakarta EE. "Jakarta Servlet Specification."