J2EE Framework: Saving Unserializable Objects to Disk
Description
J2EE Framework: Saving Unserializable Objects to Disk occurs when a J2EE application attempts to persist session data or other objects that don't implement the Serializable interface. When the application server needs to passivate sessions, replicate them across a cluster, or persist them for failover, non-serializable objects cause failures. The application may crash, lose session data, or fail to scale properly.
Risk
Session data loss can cause users to lose their work or be unexpectedly logged out. In clustered environments, sessions cannot be replicated, breaking high availability. Application servers may throw exceptions during passivation, potentially crashing the application. Security contexts stored in sessions may not persist correctly, leading to authentication issues. The application may appear to work in development but fail in production.
Solution
Ensure all objects stored in HTTP sessions implement Serializable. Make fields that shouldn't be serialized transient. Implement custom serialization for complex objects. Use serialization proxies for security-sensitive classes. Test session serialization in development. Consider using alternative session storage mechanisms that don't require Java serialization.
Common Consequences
| Impact | Details |
|---|---|
| Availability | Scope: Session Loss Sessions cannot be persisted or replicated, causing data loss. |
| Reliability | Scope: Clustering Failure Application cannot scale across multiple servers. |
| Security | Scope: Authentication Issues Security context may not persist correctly. |
Example Code + Solution Code
Vulnerable Code
// VULNERABLE: Non-serializable object in session
public class UserPreferences {
// Missing: implements Serializable
private String theme;
private String language;
private Connection dbConnection; // Not serializable anyway
// Getters and setters
}
@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"));
// BUG: UserPreferences is not Serializable!
session.setAttribute("userPrefs", prefs);
// Will fail when session is passivated
}
}
// VULNERABLE: Session bean with non-serializable field
public class ShoppingCart implements Serializable {
private static final long serialVersionUID = 1L;
private List<CartItem> items;
private DatabaseHelper dbHelper; // Not serializable!
public void addItem(CartItem item) {
items.add(item);
dbHelper.logPurchase(item); // Uses non-serializable field
}
}
// VULNERABLE: Anonymous inner class (often not serializable)
@WebServlet("/process")
public class VulnerableProcessServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response) {
HttpSession session = request.getSession();
// Anonymous class captures outer class reference
Runnable task = new Runnable() {
@Override
public void run() {
// Process something
}
};
// Anonymous inner classes may not be serializable
session.setAttribute("pendingTask", task);
}
}
// VULNERABLE: Lambda captures non-serializable state
public class VulnerableService {
private DataSource dataSource; // Not serializable
public void processRequest(HttpServletRequest request) {
HttpSession session = request.getSession();
// Lambda capturing non-serializable field
Consumer<String> processor = (data) -> {
try (Connection conn = dataSource.getConnection()) {
// Use connection
} catch (SQLException e) {
// Handle
}
};
// Will fail serialization
session.setAttribute("processor", processor);
}
}
// VULNERABLE: Entity with lazy-loaded collections
@Entity
public class VulnerableUser implements Serializable {
@Id
private Long id;
@OneToMany(fetch = FetchType.LAZY)
private List<Order> orders; // Hibernate proxy - not serializable!
// When session is serialized, proxy causes issues
}
// VULNERABLE: EJB with non-serializable state
@Stateful
@SessionScoped
public class VulnerableCartBean {
private List<Item> items = new ArrayList<>();
private Logger logger = LoggerFactory.getLogger(getClass()); // Not serializable!
public void addItem(Item item) {
items.add(item);
logger.info("Added item: {}", item);
}
// Stateful bean cannot be passivated
}
// VULNERABLE: CDI bean in session scope
@SessionScoped
@Named
public class VulnerableSessionBean {
// Missing Serializable interface!
private UserData userData;
private transient Service service; // Transient but bean itself not serializable
public void init() {
userData = loadUserData();
}
}
Fixed Code
// SAFE: Serializable session object
public class UserPreferences implements Serializable {
private static final long serialVersionUID = 1L;
private String theme;
private String language;
// Non-serializable resources marked transient
private transient Connection dbConnection;
// Getters and setters
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; }
// Custom serialization to handle transient fields
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Reinitialize transient fields after deserialization
this.dbConnection = null; // Will be lazily initialized
}
}
@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"));
// Safe: UserPreferences implements Serializable
session.setAttribute("userPrefs", prefs);
}
}
// SAFE: Shopping cart with proper serialization
public class ShoppingCart implements Serializable {
private static final long serialVersionUID = 1L;
private List<CartItem> items = new ArrayList<>();
// Mark non-serializable fields as transient
private transient DatabaseHelper dbHelper;
public void addItem(CartItem item) {
items.add(item);
getDbHelper().logPurchase(item);
}
// Lazy initialization after deserialization
private DatabaseHelper getDbHelper() {
if (dbHelper == null) {
dbHelper = new DatabaseHelper();
}
return dbHelper;
}
// Custom serialization
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Reinitialize transient fields
this.dbHelper = null;
}
}
// SAFE: Serializable 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() {
// Process using taskData
}
}
@WebServlet("/process")
public class SafeProcessServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response) {
HttpSession session = request.getSession();
// Use serializable task class instead of anonymous class
SerializableTask task = new SerializableTask("data");
session.setAttribute("pendingTask", task);
}
}
// SAFE: DTO pattern for entities
public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private List<OrderDTO> orders; // DTOs, not entities
// Convert from entity (detach from persistence context)
public static UserDTO fromEntity(User user) {
UserDTO dto = new UserDTO();
dto.id = user.getId();
dto.name = user.getName();
// Eagerly load and convert related entities
dto.orders = user.getOrders().stream()
.map(OrderDTO::fromEntity)
.collect(Collectors.toList());
return dto;
}
// Getters and setters
}
// Usage in servlet
public void storeUserInSession(HttpServletRequest request, User user) {
HttpSession session = request.getSession();
// Convert entity to DTO before storing in session
UserDTO userDto = UserDTO.fromEntity(user);
session.setAttribute("user", userDto);
}
// SAFE: Stateful EJB with proper serialization
@Stateful
@SessionScoped
public class SafeCartBean implements Serializable {
private static final long serialVersionUID = 1L;
private List<Item> items = new ArrayList<>();
// Logger is 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("Added item: {}", item);
}
@PrePassivate
public void prePassivate() {
// Clean up before passivation
logger = null;
}
@PostActivate
public void postActivate() {
// Reinitialize after activation
logger = LoggerFactory.getLogger(getClass());
}
}
// SAFE: 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; // Injected, transient
@PostConstruct
public void init() {
userData = loadUserData();
}
private UserData loadUserData() {
return userService.getCurrentUser();
}
}
// SAFE: Validate serializability in tests
public class SerializabilityTest {
@Test
public void testUserPreferencesSerializable() throws Exception {
UserPreferences prefs = new UserPreferences();
prefs.setTheme("dark");
prefs.setLanguage("en");
// Serialize
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(prefs);
oos.close();
// Deserialize
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
UserPreferences restored = (UserPreferences) ois.readObject();
ois.close();
// Verify
assertEquals("dark", restored.getTheme());
assertEquals("en", restored.getLanguage());
}
}
Exploited in the Wild
Session Loss in Production
Applications that worked in development failed in production clusters due to session replication failures.
Failover Failures
Systems failed to recover user sessions after server restarts due to non-serializable session data.
Scaling Issues
Applications could not scale horizontally because sessions couldn't be distributed.
Tools to test/exploit
-
JVM serialization testing frameworks.
-
Application server logs for NotSerializableException.
-
Clustering test environments.
-
Static analysis tools detecting Serializable issues.
CVE Examples
-
Session management vulnerabilities due to serialization failures.
-
High availability failures from non-serializable session data.
References
-
MITRE. "CWE-594: J2EE Framework: Saving Unserializable Objects to Disk." https://cwe.mitre.org/data/definitions/594.html
-
Java EE Best Practices for Session Management.