Skip to content

Commit affba70

Browse files
authored
[GEODE-10535] Secure Session Deserialization with Application-Level Security Model using ObjectInputFilter (JEP 290) (#7966)
* Add application-level security using ObjectInputFilter (JEP 290) - Implement per-application deserialization filtering using standard JEP 290 API - Add ObjectInputFilter parameter to ClassLoaderObjectInputStream constructor - Update GemfireHttpSession to read filter configuration from ServletContext - Add comprehensive security tests covering RCE and DoS prevention - Add 52 tests validating gadget chain blocking and resource limits - Add example configuration in session-testing-war web.xml This provides application-level security isolation, allowing each web application to define its own deserialization policy independent of cluster configuration. * Add ObjectInputFilter security documentation for HTTP Session Management - Add comprehensive security guide for configuring deserialization protection - Document JEP 290 ObjectInputFilter pattern syntax and examples - Include best practices, troubleshooting, and migration guidance - Add navigation link in HTTP Session Management chapter overview * Address PR review feedback: cache filter, add null check, add logging - Implement filter caching using double-checked locking with volatile fields to eliminate race conditions and improve performance - Add null check before setObjectInputFilter() for defensive programming - Add INFO logging when filter is configured and WARN logging when not configured to improve security visibility Addresses review comments by @sboorlagadda on PR #7966
1 parent 74cf647 commit affba70

File tree

8 files changed

+1647
-1
lines changed

8 files changed

+1647
-1
lines changed

extensions/geode-modules-session-internal/src/main/java/org/apache/geode/modules/session/internal/filter/GemfireHttpSession.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.DataInput;
2121
import java.io.DataOutput;
2222
import java.io.IOException;
23+
import java.io.ObjectInputFilter;
2324
import java.io.ObjectInputStream;
2425
import java.io.ObjectOutputStream;
2526
import java.util.Collections;
@@ -78,6 +79,13 @@ public class GemfireHttpSession implements HttpSession, DataSerializable, Delta
7879

7980
private ServletContext context;
8081

82+
/**
83+
* Cached ObjectInputFilter to avoid recreating on every deserialization.
84+
* Initialized lazily on first use with double-checked locking.
85+
*/
86+
private volatile ObjectInputFilter cachedFilter;
87+
private volatile boolean filterLogged = false;
88+
8189
/**
8290
* A session becomes invalid if it is explicitly invalidated or if it expires.
8391
*/
@@ -107,6 +115,34 @@ public DataSerializable newInstance() {
107115
});
108116
}
109117

118+
/**
119+
* Gets or creates the cached ObjectInputFilter. Uses double-checked locking to avoid
120+
* unnecessary synchronization after initialization.
121+
*
122+
* @return the cached ObjectInputFilter, or null if no filter is configured
123+
*/
124+
private ObjectInputFilter getOrCreateFilter() {
125+
if (cachedFilter == null && !filterLogged) {
126+
synchronized (this) {
127+
if (cachedFilter == null && !filterLogged) {
128+
String filterPattern = getServletContext()
129+
.getInitParameter("serializable-object-filter");
130+
131+
if (filterPattern != null) {
132+
cachedFilter = ObjectInputFilter.Config.createFilter(filterPattern);
133+
LOG.info("ObjectInputFilter configured with pattern: {}", filterPattern);
134+
} else {
135+
LOG.warn("No ObjectInputFilter configured. Session deserialization is not protected " +
136+
"against malicious payloads. Configure 'serializable-object-filter' in web.xml " +
137+
"to enable deserialization security.");
138+
}
139+
filterLogged = true;
140+
}
141+
}
142+
}
143+
return cachedFilter;
144+
}
145+
110146
/**
111147
* Constructor used for de-serialization
112148
*/
@@ -144,8 +180,11 @@ public Object getAttribute(String name) {
144180
oos.writeObject(obj);
145181
oos.close();
146182

183+
// Get or create cached filter for secure deserialization
184+
ObjectInputFilter filter = getOrCreateFilter();
185+
147186
ObjectInputStream ois = new ClassLoaderObjectInputStream(
148-
new ByteArrayInputStream(baos.toByteArray()), loader);
187+
new ByteArrayInputStream(baos.toByteArray()), loader, filter);
149188
tmpObj = ois.readObject();
150189
} catch (IOException | ClassNotFoundException e) {
151190
LOG.error("Exception while recreating attribute '" + name + "'", e);

extensions/geode-modules/src/main/java/org/apache/geode/modules/util/ClassLoaderObjectInputStream.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,43 @@
1616

1717
import java.io.IOException;
1818
import java.io.InputStream;
19+
import java.io.ObjectInputFilter;
1920
import java.io.ObjectInputStream;
2021
import java.io.ObjectStreamClass;
2122

2223
/**
2324
* This class is used when session attributes need to be reconstructed with a new classloader.
25+
* It now supports ObjectInputFilter for secure deserialization.
2426
*/
2527
public class ClassLoaderObjectInputStream extends ObjectInputStream {
2628

2729
private final ClassLoader loader;
2830

31+
/**
32+
* Constructs a ClassLoaderObjectInputStream with an ObjectInputFilter for secure deserialization.
33+
*
34+
* @param in the input stream to read from
35+
* @param loader the ClassLoader to use for class resolution
36+
* @param filter the ObjectInputFilter to validate deserialized classes (required for security)
37+
* @throws IOException if an I/O error occurs
38+
*/
39+
public ClassLoaderObjectInputStream(InputStream in, ClassLoader loader, ObjectInputFilter filter)
40+
throws IOException {
41+
super(in);
42+
this.loader = loader;
43+
if (filter != null) {
44+
setObjectInputFilter(filter);
45+
}
46+
}
47+
48+
/**
49+
* Legacy constructor for backward compatibility.
50+
*
51+
* @deprecated Use
52+
* {@link #ClassLoaderObjectInputStream(InputStream, ClassLoader, ObjectInputFilter)}
53+
* with a filter for secure deserialization
54+
*/
55+
@Deprecated
2956
public ClassLoaderObjectInputStream(InputStream in, ClassLoader loader) throws IOException {
3057
super(in);
3158
this.loader = loader;

extensions/geode-modules/src/test/java/org/apache/geode/modules/util/ClassLoaderObjectInputStreamTest.java

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.io.ByteArrayOutputStream;
2222
import java.io.File;
2323
import java.io.IOException;
24+
import java.io.InvalidClassException;
25+
import java.io.ObjectInputFilter;
2426
import java.io.ObjectInputStream;
2527
import java.io.ObjectOutputStream;
2628
import java.io.Serializable;
@@ -162,4 +164,142 @@ File getTempFile() {
162164
return null;
163165
}
164166
}
167+
168+
@Test
169+
public void filterRejectsUnauthorizedClasses() throws Exception {
170+
// Arrange: Create filter that only allows java.lang and java.util classes
171+
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("java.lang.*;java.util.*;!*");
172+
TestSerializable testObject = new TestSerializable("test");
173+
byte[] serializedData = serialize(testObject);
174+
175+
// Act & Assert: Deserialization should be rejected by filter
176+
assertThatThrownBy(() -> {
177+
try (ClassLoaderObjectInputStream ois = new ClassLoaderObjectInputStream(
178+
new ByteArrayInputStream(serializedData),
179+
Thread.currentThread().getContextClassLoader(),
180+
filter)) {
181+
ois.readObject();
182+
}
183+
}).isInstanceOf(InvalidClassException.class);
184+
}
185+
186+
@Test
187+
public void filterAllowsAuthorizedClasses() throws Exception {
188+
// Arrange: Create filter that allows this test class package
189+
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
190+
"java.lang.*;java.util.*;org.apache.geode.modules.util.**;!*");
191+
TestSerializable testObject = new TestSerializable("test data");
192+
byte[] serializedData = serialize(testObject);
193+
194+
// Act: Deserialize with filter
195+
Object deserialized;
196+
try (ClassLoaderObjectInputStream ois = new ClassLoaderObjectInputStream(
197+
new ByteArrayInputStream(serializedData),
198+
Thread.currentThread().getContextClassLoader(),
199+
filter)) {
200+
deserialized = ois.readObject();
201+
}
202+
203+
// Assert: Object should be successfully deserialized
204+
assertThat(deserialized).isInstanceOf(TestSerializable.class);
205+
assertThat(((TestSerializable) deserialized).getData()).isEqualTo("test data");
206+
}
207+
208+
@Test
209+
public void nullFilterAllowsAllClasses() throws Exception {
210+
// Arrange: Null filter means no filtering (backward compatibility)
211+
TestSerializable testObject = new TestSerializable("unfiltered data");
212+
byte[] serializedData = serialize(testObject);
213+
214+
// Act: Deserialize with null filter
215+
Object deserialized;
216+
try (ClassLoaderObjectInputStream ois = new ClassLoaderObjectInputStream(
217+
new ByteArrayInputStream(serializedData),
218+
Thread.currentThread().getContextClassLoader(),
219+
null)) {
220+
deserialized = ois.readObject();
221+
}
222+
223+
// Assert: Object should be successfully deserialized
224+
assertThat(deserialized).isInstanceOf(TestSerializable.class);
225+
assertThat(((TestSerializable) deserialized).getData()).isEqualTo("unfiltered data");
226+
}
227+
228+
@Test
229+
public void deprecatedConstructorStillWorks() throws Exception {
230+
// Arrange: Use deprecated constructor without filter
231+
TestSerializable testObject = new TestSerializable("legacy code");
232+
byte[] serializedData = serialize(testObject);
233+
234+
// Act: Deserialize using deprecated constructor
235+
Object deserialized;
236+
try (ClassLoaderObjectInputStream ois = new ClassLoaderObjectInputStream(
237+
new ByteArrayInputStream(serializedData),
238+
Thread.currentThread().getContextClassLoader())) {
239+
deserialized = ois.readObject();
240+
}
241+
242+
// Assert: Object should be successfully deserialized (backward compatibility)
243+
assertThat(deserialized).isInstanceOf(TestSerializable.class);
244+
assertThat(((TestSerializable) deserialized).getData()).isEqualTo("legacy code");
245+
}
246+
247+
@Test
248+
public void filterEnforcesResourceLimits() throws Exception {
249+
// Arrange: Create filter with very low depth limit
250+
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("maxdepth=2;*");
251+
NestedSerializable nested = new NestedSerializable(
252+
new NestedSerializable(
253+
new NestedSerializable(null))); // Depth of 3
254+
byte[] serializedData = serialize(nested);
255+
256+
// Act & Assert: Should reject due to depth limit
257+
assertThatThrownBy(() -> {
258+
try (ClassLoaderObjectInputStream ois = new ClassLoaderObjectInputStream(
259+
new ByteArrayInputStream(serializedData),
260+
Thread.currentThread().getContextClassLoader(),
261+
filter)) {
262+
ois.readObject();
263+
}
264+
}).isInstanceOf(InvalidClassException.class);
265+
}
266+
267+
/**
268+
* Helper method to serialize an object to byte array
269+
*/
270+
private byte[] serialize(Object obj) throws IOException {
271+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
272+
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
273+
oos.writeObject(obj);
274+
}
275+
return baos.toByteArray();
276+
}
277+
278+
/**
279+
* Test class for serialization testing
280+
*/
281+
static class TestSerializable implements Serializable {
282+
private static final long serialVersionUID = 1L;
283+
private final String data;
284+
285+
TestSerializable(String data) {
286+
this.data = data;
287+
}
288+
289+
String getData() {
290+
return data;
291+
}
292+
}
293+
294+
/**
295+
* Nested test class for depth limit testing
296+
*/
297+
static class NestedSerializable implements Serializable {
298+
private static final long serialVersionUID = 1L;
299+
private final NestedSerializable nested;
300+
301+
NestedSerializable(NestedSerializable nested) {
302+
this.nested = nested;
303+
}
304+
}
165305
}

0 commit comments

Comments
 (0)