Skip to content

Commit 7aad894

Browse files
committed
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.
1 parent f97ffde commit 7aad894

6 files changed

Lines changed: 1285 additions & 1 deletion

File tree

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

Lines changed: 9 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;
@@ -144,8 +145,15 @@ public Object getAttribute(String name) {
144145
oos.writeObject(obj);
145146
oos.close();
146147

148+
// Create filter from user configuration for secure deserialization
149+
String filterPattern = getServletContext()
150+
.getInitParameter("serializable-object-filter");
151+
ObjectInputFilter filter = filterPattern != null
152+
? ObjectInputFilter.Config.createFilter(filterPattern)
153+
: null;
154+
147155
ObjectInputStream ois = new ClassLoaderObjectInputStream(
148-
new ByteArrayInputStream(baos.toByteArray()), loader);
156+
new ByteArrayInputStream(baos.toByteArray()), loader, filter);
149157
tmpObj = ois.readObject();
150158
} catch (IOException | ClassNotFoundException e) {
151159
LOG.error("Exception while recreating attribute '" + name + "'", e);

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,41 @@
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+
setObjectInputFilter(filter);
44+
}
45+
46+
/**
47+
* Legacy constructor for backward compatibility.
48+
*
49+
* @deprecated Use
50+
* {@link #ClassLoaderObjectInputStream(InputStream, ClassLoader, ObjectInputFilter)}
51+
* with a filter for secure deserialization
52+
*/
53+
@Deprecated
2954
public ClassLoaderObjectInputStream(InputStream in, ClassLoader loader) throws IOException {
3055
super(in);
3156
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)