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
| Impact | Details |
|---|---|
| Availability | Scope: Availability DoS: Crash, Exit, or Restart - Session replication failures can cause request failures and service disruptions during failover events. |
| Other | Scope: 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
- 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."