J2EE Bad Practices: Non-serializable Object Stored in Session

Description

J2EE Bad Practices: Non-serializable Object Stored in Session is a vulnerability where a Java web application stores objects that do not implement the Serializable interface as HttpSession attributes. In J2EE environments, containers often replicate HttpSession objects across multiple JVMs for load balancing and failover purposes. This replication requires that all session attributes be serializable so they can be transmitted between servers. When non-serializable objects are stored in sessions, the replication fails, causing session data loss during failover, load balancing failures, and unpredictable application behavior.

Risk

Storing non-serializable objects in sessions creates significant reliability and availability risks. During server failover, users lose their session state, forcing re-authentication and loss of in-progress work. Load balancers cannot distribute requests freely across servers, reducing scalability. Session persistence to disk fails, meaning server restarts lose all sessions. The application may appear to work in development with a single server but fail catastrophically in production clusters. Some containers throw exceptions when attempting to replicate non-serializable sessions, causing request failures. Users experience inconsistent behavior depending on which server handles their requests.

Solution

Ensure all objects stored in HttpSession implement the Serializable interface. For complex objects, implement proper serialization with serialVersionUID fields. Avoid storing non-serializable resources like database connections, threads, or I/O streams in sessions; instead, store identifiers and recreate resources as needed. Use transient keyword for fields that cannot or should not be serialized. Consider using wrapper classes that implement Serializable for third-party objects. Test session replication in development using clustered configurations. Use static analysis tools to detect non-serializable session attributes.

Common Consequences

ImpactDetails
AvailabilityScope: Availability

DoS: Crash, Exit, or Restart - Session replication failures can cause request failures and service disruptions during failover events.
OtherScope: Other

Quality Degradation - Applications become unreliable in clustered deployments, failing during failover or load balancing.

Example Code

Vulnerable Code

// Vulnerable: Non-serializable class stored in session
public class UserPreferences {
    // Missing: 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<>();
    }

    // Getters and setters...
}

// Vulnerable: Servlet storing non-serializable object
@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();

            // Vulnerable: UserPreferences is not Serializable
            UserPreferences prefs = loadUserPreferences(username);
            session.setAttribute("userPrefs", prefs);  // Replication fails!

            // Vulnerable: Storing non-serializable database connection
            Connection conn = getConnection();
            session.setAttribute("dbConnection", conn);  // Cannot serialize!

            // Vulnerable: Storing thread reference
            Thread backgroundTask = new Thread(() -> doBackgroundWork());
            session.setAttribute("task", backgroundTask);  // Not serializable!

            response.sendRedirect("/dashboard");
        }
    }
}

// Vulnerable: Class with non-serializable fields
public class ShoppingCart implements Serializable {
    private static final long serialVersionUID = 1L;

    private List<CartItem> items;  // OK if CartItem is Serializable
    private Connection dbConnection;  // Vulnerable: Connection not serializable
    private Logger logger;  // Vulnerable: Logger often not serializable
    private Thread priceUpdateThread;  // Vulnerable: Thread not serializable

    public ShoppingCart() {
        this.items = new ArrayList<>();
        this.dbConnection = getConnection();  // Will fail during serialization
        this.logger = LoggerFactory.getLogger(ShoppingCart.class);
    }
}

// Vulnerable: Storing complex object graphs with non-serializable members
public class UserSession {
    // Missing: implements Serializable
    private User user;
    private Map<String, Object> attributes;  // Object could be non-serializable
    private InputStream uploadStream;  // Not serializable
    private Socket socket;  // Not serializable

    public void setAttribute(String key, Object value) {
        // Vulnerable: No check if value is serializable
        attributes.put(key, value);
    }
}

// Vulnerable: JSF managed bean not serializable
@Named
@SessionScoped
public class VulnerableUserBean {
    // Missing: implements Serializable

    private String username;
    private List<Message> messages;
    private Connection connection;  // Non-serializable field

    // Session scoped but not serializable - replication fails
}

Fixed Code

// Fixed: Serializable session object
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 is Serializable

    public UserPreferences(String theme, String language) {
        this.theme = theme;
        this.language = language;
        this.recentItems = new ArrayList<>();
    }

    // Getters and setters...
}

// Fixed: Servlet storing only serializable objects
@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();

            // Fixed: UserPreferences now implements Serializable
            UserPreferences prefs = loadUserPreferences(username);
            session.setAttribute("userPrefs", prefs);

            // Fixed: Store user ID instead of connection
            session.setAttribute("userId", getUserId(username));

            // Fixed: Store task ID, not thread reference
            String taskId = startBackgroundTask(username);
            session.setAttribute("backgroundTaskId", taskId);

            response.sendRedirect("/dashboard");
        }
    }
}

// Fixed: Properly serializable class with transient fields
public class ShoppingCart implements Serializable {
    private static final long serialVersionUID = 1L;

    private List<CartItem> items;  // CartItem must be Serializable

    // Fixed: Mark non-serializable fields as transient
    private transient Connection dbConnection;
    private transient Logger logger;

    // Fixed: Don't store thread - use task management
    private String priceUpdateTaskId;

    public ShoppingCart() {
        this.items = new ArrayList<>();
    }

    // Fixed: Reinitialize transient fields after deserialization
    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        // Reinitialize transient fields
        this.logger = LoggerFactory.getLogger(ShoppingCart.class);
        // dbConnection should be obtained when needed, not stored
    }

    // Fixed: Get connection when needed, don't store it
    private Connection getConnection() {
        // Obtain from pool each time
        return DataSourceProvider.getConnection();
    }

    public void addItem(CartItem item) {
        items.add(item);
        // Use fresh connection
        try (Connection conn = getConnection()) {
            saveToDatabase(conn, item);
        } catch (SQLException e) {
            logger.error("Failed to save cart item", e);
        }
    }
}

// Fixed: Serializable cart item
public class CartItem implements Serializable {
    private static final long serialVersionUID = 1L;

    private String productId;
    private String productName;
    private int quantity;
    private BigDecimal price;  // BigDecimal is Serializable

    // All fields are serializable primitives or serializable objects
}

// Fixed: Session class with proper serialization
public class UserSession implements Serializable {
    private static final long serialVersionUID = 1L;

    private User user;  // User must implement Serializable
    private Map<String, Serializable> attributes;  // Only Serializable values

    // Fixed: Store reference ID instead of non-serializable resource
    private String uploadId;  // Reference to upload, not stream
    private transient InputStream uploadStream;

    public UserSession() {
        this.attributes = new HashMap<>();
    }

    // Fixed: Type-safe method ensuring serializability
    public void setAttribute(String key, Serializable value) {
        attributes.put(key, value);
    }

    // Fixed: Generic method with runtime check
    public void setAttributeChecked(String key, Object value) {
        if (value != null && !(value instanceof Serializable)) {
            throw new IllegalArgumentException(
                "Session attribute must be Serializable: " + key);
        }
        attributes.put(key, (Serializable) value);
    }
}

// Fixed: User class implementing 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 is Serializable
    private Date lastLogin;  // Date is Serializable

    // Don't store password hash in session
    // transient private String passwordHash;
}

// Fixed: JSF managed bean with proper serialization
@Named
@SessionScoped
public class SecureUserBean implements Serializable {
    private static final long serialVersionUID = 1L;

    private String username;
    private List<Message> messages;  // Message must be Serializable

    // Fixed: Don't store connection - use injected data source
    @Inject
    private transient DataSource dataSource;

    // Reinitialize after deserialization
    @PostConstruct
    public void init() {
        // Initialize or load data
    }
}

// Fixed: Wrapper for third-party non-serializable objects
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;
    }
}

// Fixed: Session listener to validate serialization
@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("Non-serializable object stored in session: " +
                event.getName() + " = " + value.getClass().getName());

            // In development, you might want to throw an exception
            // throw new IllegalArgumentException(
            //     "Session attribute must be Serializable");
        }
    }
}

CVE Examples

No specific CVEs are commonly attributed to this CWE, as it primarily affects application reliability rather than security vulnerabilities.


References

  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."