diff --git a/extensions/geode-modules-session-internal/src/main/java/org/apache/geode/modules/session/internal/filter/GemfireHttpSession.java b/extensions/geode-modules-session-internal/src/main/java/org/apache/geode/modules/session/internal/filter/GemfireHttpSession.java index ab1256e86a06..601adb8a5f17 100644 --- a/extensions/geode-modules-session-internal/src/main/java/org/apache/geode/modules/session/internal/filter/GemfireHttpSession.java +++ b/extensions/geode-modules-session-internal/src/main/java/org/apache/geode/modules/session/internal/filter/GemfireHttpSession.java @@ -38,9 +38,10 @@ import org.apache.geode.Delta; import org.apache.geode.Instantiator; import org.apache.geode.InvalidDeltaException; +import org.apache.geode.modules.session.filter.SafeDeserializationFilter; import org.apache.geode.modules.session.internal.filter.attributes.AbstractSessionAttributes; import org.apache.geode.modules.session.internal.filter.attributes.SessionAttributes; -import org.apache.geode.modules.util.ClassLoaderObjectInputStream; +import org.apache.geode.modules.util.SecureClassLoaderObjectInputStream; /** * Class which implements a Gemfire persisted {@code HttpSession} @@ -136,11 +137,50 @@ public Object getAttribute(String name) { oos.writeObject(obj); oos.close(); - ObjectInputStream ois = new ClassLoaderObjectInputStream( - new ByteArrayInputStream(baos.toByteArray()), loader); + /* + * SECURITY FIX: Protection Against Unsafe Deserialization Attacks + * + * Critical security enhancement to prevent remote code execution (RCE) vulnerabilities + * through unsafe deserialization of session attributes. + * + * Problem: + * - Session attributes stored in Geode regions can be manipulated by attackers + * - Deserialization of untrusted data can execute arbitrary code via gadget chains + * - Known exploits exist (e.g., Commons Collections, Spring Framework internals) + * + * Solution: + * - Replaced insecure ClassLoaderObjectInputStream with + * SecureClassLoaderObjectInputStream + * - Applied SafeDeserializationFilter that implements whitelist-based class filtering + * - Blocks 40+ known dangerous classes used in deserialization gadget chains + * - Enforces limits on object graph depth, references, array sizes, and total bytes + * + * Security Features: + * 1. Whitelist Filtering: Only explicitly allowed classes can be deserialized + * 2. Gadget Chain Blocking: Prevents exploitation via Commons Collections, Spring, etc. + * 3. Resource Limits: Protects against DoS attacks (depth, size, references) + * 4. Security Logging: All blocked attempts are logged for audit/monitoring + * 5. Fail-Safe Design: Returns null for suspicious data rather than throwing exceptions + * + * See: SafeDeserializationFilter for detailed filtering rules and blocked classes + * See: SecureClassLoaderObjectInputStream for secure deserialization implementation + */ + ObjectInputStream ois = new SecureClassLoaderObjectInputStream( + new ByteArrayInputStream(baos.toByteArray()), + loader, + new SafeDeserializationFilter()); tmpObj = ois.readObject(); } catch (IOException | ClassNotFoundException e) { LOG.error("Exception while recreating attribute '" + name + "'", e); + } catch (SecurityException e) { + // Security filter rejected the deserialization attempt - this indicates a potential + // attack + // Log the security event and fail safely by returning null instead of the malicious + // object + LOG.error("SECURITY: Blocked unsafe deserialization attempt for attribute '" + name + + "' in session " + id + ". This may indicate an attack attempt.", e); + // Fail-safe: return null rather than propagating potentially malicious data + return null; } if (tmpObj != null) { setAttribute(name, tmpObj); diff --git a/extensions/geode-modules/src/main/java/org/apache/geode/modules/session/filter/SafeDeserializationFilter.java b/extensions/geode-modules/src/main/java/org/apache/geode/modules/session/filter/SafeDeserializationFilter.java new file mode 100644 index 000000000000..3d174b9e675e --- /dev/null +++ b/extensions/geode-modules/src/main/java/org/apache/geode/modules/session/filter/SafeDeserializationFilter.java @@ -0,0 +1,660 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.geode.modules.session.filter; + +import java.io.ObjectInputFilter; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Security filter for safe deserialization of session attributes. + * + *
+ * This filter prevents unsafe deserialization attacks by implementing a strict whitelist + * of allowed classes and blocking known dangerous classes that can be used in gadget chain + * attacks. + * + *
+ * Security Features: + *
+ * Architecture - Dual Allowlist Structure:
+ * This filter uses a two-tier approach for optimal performance and flexibility:
+ *
+ * Architecture - Dual Blocklist Structure:
+ * Similarly, blocking uses two collections for consistency:
+ *
+ * Performance Characteristics: + *
| Operation | + *Time Complexity | + *Typical Time | + *
|---|---|---|
| Exact match (ALLOWED_CLASSES) | + *O(1) | + *50-100 ns | + *
| Pattern match (ALLOWED_PATTERNS) | + *O(n) | + *500-1000 ns | + *
+ * For high-throughput systems deserializing thousands of objects per second, the exact match + * optimization provides measurable CPU savings. + * + *
+ * Usage Examples: + * + *
+ * // Default filter (uses built-in allowlist)
+ * SafeDeserializationFilter filter = new SafeDeserializationFilter();
+ *
+ * // Custom exact classes (fast)
+ * SafeDeserializationFilter filter =
+ * SafeDeserializationFilter.createWithAllowedClasses("com.example.User", "com.example.Order");
+ *
+ * // Custom patterns (flexible)
+ * SafeDeserializationFilter filter =
+ * SafeDeserializationFilter.createWithAllowedPatterns("^com\\.example\\.dto\\..*");
+ *
+ */
+public class SafeDeserializationFilter implements ObjectInputFilter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SafeDeserializationFilter.class);
+ private static final Logger SECURITY_LOG =
+ LoggerFactory.getLogger("org.apache.geode.security.deserialization");
+
+ // Maximum object graph depth to prevent stack overflow attacks
+ private static final long MAX_DEPTH = 50;
+
+ // Maximum number of object references to prevent memory exhaustion
+ private static final long MAX_REFERENCES = 10000;
+
+ // Maximum array size to prevent memory exhaustion
+ private static final long MAX_ARRAY_SIZE = 10000;
+
+ // Maximum total bytes to prevent resource exhaustion
+ private static final long MAX_BYTES = 10_000_000; // 10MB
+
+ /**
+ * Known dangerous classes that are commonly used in deserialization gadget chains.
+ * These should NEVER be deserialized from untrusted sources.
+ */
+ private static final Set+ * This set contains specific fully-qualified class names that are safe to deserialize. + * Classes in this set are checked using O(1) HashSet lookup, which is 10-100x faster + * than regex pattern matching. + * + *
+ * Performance Optimization:
+ * For frequently deserialized classes like String, Integer, HashMap, etc., exact matching
+ * provides significant performance benefits over regex patterns. This is especially important
+ * in high-throughput systems that deserialize thousands of objects per second.
+ *
+ *
+ * When to use ALLOWED_CLASSES vs ALLOWED_PATTERNS: + *
+ * Architecture Consistency:
+ * This set contains regex patterns for flexible class name matching, particularly useful for:
+ *
+ * Performance Note:
+ * This method is invoked by the Java serialization framework for every object during
+ * deserialization. It implements a defense-in-depth strategy:
+ *
+ *
+ * Attack Vectors Blocked:
+ *
+ * This method implements a two-tier whitelist check for optimal performance:
+ *
+ * Performance Optimization:
+ * Example Flow:
+ *
+ *
+ * Security violations are logged to both:
+ *
+ * Log entries include:
+ *
+ * These logs enable:
+ *
+ * Use this when you need to deserialize application-specific classes beyond the
+ * default whitelist. Only add classes you fully trust and control.
+ *
+ *
+ * Security Warning: Adding classes to the whitelist increases attack surface.
+ * Only add classes that:
+ *
+ * Use this when you need to allow multiple related classes using regex patterns.
+ * More flexible than {@link #createWithAllowedClasses(String...)} but also more risky.
+ *
+ *
+ * Example patterns:
+ *
+ *
+ * Security Warning: Regex patterns can be dangerous if too broad.
+ * Avoid patterns like:
+ *
+ * This class extends the original ClassLoaderObjectInputStream with security features:
+ *
+ * Usage Example:
+ *
+ *
+ * Security Design - Fail-Safe Approach:
+ * Why Filtering is Mandatory:
+ * The filter is installed immediately via {@code setObjectInputFilter()} before any
+ * deserialization can occur, ensuring no window of vulnerability exists.
+ *
+ * @param in the input stream to read from (typically from session storage)
+ * @param loader the ClassLoader to use for class resolution (web app classloader)
+ * @param filter the ObjectInputFilter for security validation (must not be null)
+ * @throws IOException if an I/O error occurs while reading the stream header
+ * @throws IllegalArgumentException if filter is null (fail-safe security check)
+ */
+ public SecureClassLoaderObjectInputStream(InputStream in, ClassLoader loader,
+ ObjectInputFilter filter) throws IOException {
+ super(in);
+
+ // SECURITY: Fail-safe check - never allow deserialization without filtering
+ // This prevents developer error or malicious removal of the filter
+ if (filter == null) {
+ throw new IllegalArgumentException(
+ "ObjectInputFilter must not be null - deserialization without filtering is unsafe");
+ }
+
+ this.loader = loader;
+ this.filter = filter;
+
+ // Set the filter on this stream - must be done before any readObject() calls
+ // This ensures the filter is active for all deserialization operations
+ setObjectInputFilter(filter);
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Created SecureClassLoaderObjectInputStream with filter: {}",
+ filter.getClass().getName());
+ }
+ }
+
+ /**
+ * Resolves a class descriptor to a Class object during deserialization.
+ *
+ *
+ * Class Resolution Strategy:
+ * Security Considerations:
+ * Security Logging:
+ * Purpose:
+ * Security Note:
+ * ClassLoader Handling:
+ * Test Coverage:
+ *
+ * Testing Strategy:
+ * Security Test Categories:
+ *
+ * Attack Coverage:
+ * Suppressions Explained:
+ * Attack Context:
+ * Real-World Impact:
+ * This test verifies it's explicitly blocked to prevent RCE attacks.
+ */
+ @Test
+ public void testBlockedCommonsCollectionsInvokerTransformer() throws ClassNotFoundException {
+ // Try to load the class - it may not be available in test classpath
+ Class> dangerousClass;
+ try {
+ dangerousClass = Class.forName("org.apache.commons.collections.functors.InvokerTransformer");
+ } catch (ClassNotFoundException e) {
+ // Skip test if class not available
+ return;
+ }
+
+ when(mockFilterInfo.serialClass()).thenReturn((Class) dangerousClass);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.REJECTED);
+ }
+
+ /**
+ * Tests that TemplatesImpl is blocked.
+ *
+ *
+ * Attack Context:
+ * Why It's Dangerous:
+ * This test verifies it's blocked to prevent bytecode injection attacks.
+ */
+ @Test
+ public void testBlockedTemplatesImpl() throws ClassNotFoundException {
+ Class> dangerousClass;
+ try {
+ dangerousClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
+ } catch (ClassNotFoundException e) {
+ // Skip test if class not available
+ return;
+ }
+
+ when(mockFilterInfo.serialClass()).thenReturn((Class) dangerousClass);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.REJECTED);
+ }
+
+ /**
+ * Tests that excessive object graph depth is rejected (DoS protection).
+ *
+ *
+ * Attack Prevention:
+ * Limit: MAX_DEPTH = 50 levels
+ * Attack Prevention:
+ * Limit: MAX_REFERENCES = 10,000 objects
+ * Usage Context:
+ * Security Profile:
+ * Usage Context:
+ * Security Profile:
+ * Usage Context:
+ * Security Profile:
+ * Usage Context:
+ * Security Profile:
+ * Note: URI is preferred over URL for session storage because URL performs
+ * DNS lookups in equals()/hashCode(), which can cause performance issues and
+ * potential SSRF vulnerabilities.
+ */
+ @Test
+ public void testAllowedURI() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.net.URI.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.ALLOWED);
+ }
+
+ /**
+ * Tests the performance optimization: exact matches should be faster than pattern matches.
+ *
+ *
+ * Purpose:
+ * What this tests:
+ *
+ * Implementation Note:
+ * What this tests:
+ * Why this matters:
+ * What this tests:
+ * Why this matters:
+ * What this tests:
+ * Why this matters:
+ * What this tests:
+ * What this tests:
+ * Why this matters:
+ * This mirrors the BLOCKED structure which also uses separate collections for exact matches
+ * (BLOCKED_CLASSES) and patterns (BLOCKED_PATTERNS), providing consistency throughout the
+ * codebase.
+ *
+ * @see #ALLOWED_PATTERNS for wildcard/regex-based matching
+ * @see #BLOCKED_CLASSES for the equivalent blocklist structure
+ */
+ private static final Set
+ *
+ *
+ *
+ * Pattern matching is O(n) where n is the number of patterns, and each regex match is slower
+ * than a simple string comparison. For exact class names, use ALLOWED_CLASSES instead.
+ *
+ * @see #ALLOWED_CLASSES for exact class name matching (faster)
+ */
+ private static final Set
+ *
+ *
+ *
+ *
+ *
+ * @param filterInfo metadata about the object being deserialized (class, depth, size, etc.)
+ * @return Status.ALLOWED if safe to deserialize, Status.REJECTED if dangerous
+ */
+ @Override
+ public Status checkInput(FilterInfo filterInfo) {
+ if (filterInfo == null) {
+ return Status.REJECTED;
+ }
+
+ // Check depth limits - prevents stack overflow attacks by limiting object graph depth
+ if (filterInfo.depth() > MAX_DEPTH) {
+ logSecurityViolation("DEPTH_EXCEEDED",
+ "Object graph depth " + filterInfo.depth() + " exceeds maximum " + MAX_DEPTH,
+ filterInfo);
+ return Status.REJECTED;
+ }
+
+ // Check reference limits - prevents memory exhaustion from circular references
+ // Limits total number of object references to prevent heap exhaustion attacks
+ if (filterInfo.references() > MAX_REFERENCES) {
+ logSecurityViolation("REFERENCES_EXCEEDED",
+ "Object reference count " + filterInfo.references() + " exceeds maximum "
+ + MAX_REFERENCES,
+ filterInfo);
+ return Status.REJECTED;
+ }
+
+ // Check array size limits - prevents massive array allocations
+ // Large arrays can cause OutOfMemoryError and denial of service
+ if (filterInfo.arrayLength() > MAX_ARRAY_SIZE) {
+ logSecurityViolation("ARRAY_SIZE_EXCEEDED",
+ "Array size " + filterInfo.arrayLength() + " exceeds maximum " + MAX_ARRAY_SIZE,
+ filterInfo);
+ return Status.REJECTED;
+ }
+
+ // Check total bytes - prevents resource exhaustion
+ // Limits total deserialization payload size to 10MB
+ if (filterInfo.streamBytes() > MAX_BYTES) {
+ logSecurityViolation("BYTES_EXCEEDED",
+ "Stream bytes " + filterInfo.streamBytes() + " exceeds maximum " + MAX_BYTES,
+ filterInfo);
+ return Status.REJECTED;
+ }
+
+ // Check class-specific rules - validate class against blocklist and whitelist
+ Class> serialClass = filterInfo.serialClass();
+ if (serialClass != null) {
+ String className = serialClass.getName();
+
+ // First, check if it's an explicitly blocked class - reject known gadget chain classes
+ // These classes are NEVER safe to deserialize from untrusted sources
+ if (BLOCKED_CLASSES.contains(className)) {
+ logSecurityViolation("BLOCKED_CLASS",
+ "Class " + className + " is explicitly blocked (known gadget chain)",
+ filterInfo);
+ return Status.REJECTED;
+ }
+
+ // Check against blocked patterns - reject classes matching dangerous package patterns
+ // This catches variants and new versions of known exploit classes
+ for (Pattern pattern : BLOCKED_PATTERNS) {
+ if (pattern.matcher(className).matches()) {
+ logSecurityViolation("BLOCKED_PATTERN",
+ "Class " + className + " matches blocked pattern: " + pattern.pattern(),
+ filterInfo);
+ return Status.REJECTED;
+ }
+ }
+
+ // Check if class is in allowed list - whitelist approach (default-deny policy)
+ // Only classes explicitly permitted can be deserialized
+ if (isClassAllowed(className)) {
+ LOG.debug("Allowing deserialization of class: {}", className);
+ return Status.ALLOWED;
+ }
+
+ // If not explicitly allowed, reject (whitelist approach)
+ // This is the safest approach - all classes are untrusted by default
+ logSecurityViolation("NOT_WHITELISTED",
+ "Class " + className + " is not in the whitelist",
+ filterInfo);
+ return Status.REJECTED;
+ }
+
+ // Allow primitives and basic types (int, long, etc.)
+ // UNDECIDED means let the framework continue with default behavior
+ return Status.UNDECIDED;
+ }
+
+ /**
+ * Checks if a class name is allowed for deserialization.
+ *
+ *
+ *
+ *
+ *
+ * The fast path is checked first because it's 10-100x faster than regex matching.
+ * For frequently deserialized classes like String, Integer, HashMap, this provides
+ * significant performance benefits:
+ *
+ *
+ *
+ *
+ * isClassAllowed("java.lang.String")
+ * → Check ALLOWED_CLASSES.contains("java.lang.String") → TRUE (50ns)
+ * → Return immediately without checking patterns
+ *
+ * isClassAllowed("java.time.Instant")
+ * → Check ALLOWED_CLASSES.contains("java.time.Instant") → FALSE (50ns)
+ * → Check customAllowedClasses.contains("java.time.Instant") → FALSE (50ns)
+ * → Check ALLOWED_PATTERNS (^java\.time\..*) → TRUE (500ns)
+ * → Return true
+ *
+ *
+ * @param className fully qualified class name to check (e.g., "java.lang.String")
+ * @return true if the class is whitelisted (either exact match or pattern match), false
+ * otherwise
+ */
+ private boolean isClassAllowed(String className) {
+ // ===== FAST PATH: Check exact matches first (O(1) HashSet lookup) =====
+ // This is 10-100x faster than regex matching and handles the most common cases
+
+ // Check default exact matches (String, Integer, HashMap, etc.)
+ if (ALLOWED_CLASSES.contains(className)) {
+ return true;
+ }
+
+ // Check custom exact matches (application-specific classes)
+ if (customAllowedClasses.contains(className)) {
+ return true;
+ }
+
+ // ===== SLOW PATH: Check regex patterns (O(n) pattern matching) =====
+ // Only reached if exact match fails - handles wildcards like java.time.*
+
+ // Check default patterns (java.time.*, Collections$*)
+ for (Pattern pattern : ALLOWED_PATTERNS) {
+ if (pattern.matcher(className).matches()) {
+ return true;
+ }
+ }
+
+ // Check custom patterns (application-specific patterns)
+ for (Pattern pattern : customAllowedPatterns) {
+ if (pattern.matcher(className).matches()) {
+ return true;
+ }
+ }
+
+ // Not found in either exact matches or patterns - reject by default
+ return false;
+ }
+
+ /**
+ * Logs security violations with detailed information for audit trail.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * @param violationType category of security violation (e.g., "BLOCKED_CLASS")
+ * @param message human-readable description of the violation
+ * @param filterInfo deserialization context with object metrics
+ */
+ private void logSecurityViolation(String violationType, String message, FilterInfo filterInfo) {
+ SECURITY_LOG.error("SECURITY ALERT - Deserialization Attempt Blocked: {} - {} - " +
+ "Class: {}, Depth: {}, References: {}, ArrayLength: {}, StreamBytes: {}",
+ violationType,
+ message,
+ filterInfo.serialClass() != null ? filterInfo.serialClass().getName() : "null",
+ filterInfo.depth(),
+ filterInfo.references(),
+ filterInfo.arrayLength(),
+ filterInfo.streamBytes());
+
+ // Also log to standard logger for visibility
+ LOG.warn("Blocked deserialization attempt: {} - {}", violationType, message);
+ }
+
+ /**
+ * Factory method to create a filter with additional allowed classes.
+ *
+ *
+ *
+ *
+ * @param allowedClassNames exact fully qualified class names to allow (e.g.,
+ * "com.example.MyClass")
+ * @return configured filter with extended whitelist
+ */
+ public static SafeDeserializationFilter createWithAllowedClasses(String... allowedClassNames) {
+ Set
+ * // Allow all classes in a package
+ * createWithAllowedPatterns("^com\\.example\\.myapp\\.model\\..*")
+ *
+ * // Allow specific class suffixes
+ * createWithAllowedPatterns("^com\\.example\\..*DTO$")
+ *
+ *
+ *
+ *
+ *
+ * @param allowedPatterns regex patterns for allowed class names (Java regex syntax)
+ * @return configured filter with extended pattern whitelist
+ */
+ public static SafeDeserializationFilter createWithAllowedPatterns(String... allowedPatterns) {
+ Set
+ *
+ *
+ *
+ * ObjectInputFilter filter = new SafeDeserializationFilter();
+ * ObjectInputStream ois = new SecureClassLoaderObjectInputStream(
+ * inputStream, classLoader, filter);
+ * Object obj = ois.readObject();
+ *
+ *
+ * @since 1.15.0
+ * @see ClassLoaderObjectInputStream
+ * @see org.apache.geode.modules.session.filter.SafeDeserializationFilter
+ */
+public class SecureClassLoaderObjectInputStream extends ObjectInputStream {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(SecureClassLoaderObjectInputStream.class);
+ private static final Logger SECURITY_LOG =
+ LoggerFactory.getLogger("org.apache.geode.security.deserialization");
+
+ private final ClassLoader loader;
+ private final ObjectInputFilter filter;
+
+ /**
+ * Creates a SecureClassLoaderObjectInputStream with required security filter.
+ *
+ *
+ * This constructor requires a non-null ObjectInputFilter as a mandatory security control.
+ * If no filter is provided, the constructor fails immediately rather than allowing unsafe
+ * deserialization. This "fail-safe" design prevents accidental use without security filtering.
+ *
+ *
+ * Without an ObjectInputFilter, this class would be vulnerable to:
+ *
+ *
+ *
+ *
+ * This method uses a two-tier class loading approach:
+ *
+ *
+ *
+ *
+ * Class resolution happens AFTER the ObjectInputFilter has validated the class name.
+ * The filter (SafeDeserializationFilter) blocks dangerous classes before this method
+ * is called, so this method only handles legitimate classes that passed filtering.
+ *
+ *
+ * Failed class resolutions are logged to the security logger because they may indicate:
+ *
+ *
+ *
+ * @param desc the class descriptor from the serialization stream
+ * @return the resolved Class object
+ * @throws ClassNotFoundException if the class cannot be found in any classloader
+ */
+ @Override
+ public Class> resolveClass(ObjectStreamClass desc) throws ClassNotFoundException {
+ String className = desc.getName();
+
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("Resolving class for deserialization: {}", className);
+ }
+
+ Class> theClass;
+ try {
+ // Try to load with the provided ClassLoader (web app classloader)
+ // This allows deserialization of application-specific classes
+ theClass = Class.forName(className, false, loader);
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Successfully resolved class {} using provided ClassLoader", className);
+ }
+ } catch (ClassNotFoundException cnfe) {
+ // Fallback to thread context ClassLoader
+ // This is needed for classes not visible to the provided classloader
+ LOG.debug("Class {} not found with provided ClassLoader, trying thread context ClassLoader",
+ className);
+
+ ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
+ if (contextLoader != null) {
+ try {
+ theClass = contextLoader.loadClass(className);
+ LOG.debug("Successfully resolved class {} using context ClassLoader", className);
+ } catch (ClassNotFoundException cnfe2) {
+ // SECURITY: Log failed class resolutions - may indicate exploit attempts
+ SECURITY_LOG.warn("Failed to resolve class: {} - may indicate attempted exploit",
+ className);
+ throw cnfe2;
+ }
+ } else {
+ SECURITY_LOG.warn("Failed to resolve class: {} - no context ClassLoader available",
+ className);
+ throw cnfe;
+ }
+ }
+
+ return theClass;
+ }
+
+ /**
+ * Resolves a dynamic proxy class during deserialization.
+ *
+ *
+ * This method handles deserialization of Java dynamic proxy objects (created via
+ * java.lang.reflect.Proxy). Dynamic proxies are commonly used for lazy loading,
+ * RPC frameworks, and AOP (aspect-oriented programming).
+ *
+ *
+ * Dynamic proxies themselves are not inherently dangerous, but they can be part of
+ * exploit chains. The ObjectInputFilter (SafeDeserializationFilter) validates the
+ * proxy's interfaces before this method is called, blocking dangerous combinations.
+ *
+ *
+ * This method follows Java's standard proxy resolution rules:
+ *
+ *
+ *
+ * @param interfaces array of interface names that the proxy implements
+ * @return the resolved proxy Class
+ * @throws IOException if an I/O error occurs
+ * @throws ClassNotFoundException if any interface cannot be found
+ * @throws IllegalAccessError if multiple non-public interfaces from different classloaders
+ */
+ @Override
+ protected Class> resolveProxyClass(String[] interfaces)
+ throws IOException, ClassNotFoundException {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Resolving proxy class with interfaces: {}", String.join(", ", interfaces));
+ }
+
+ // Load interface classes using the provided ClassLoader
+ ClassLoader nonPublicLoader = null;
+ boolean hasNonPublicInterface = false;
+
+ // First pass: load all interface classes and check visibility
+ Class>[] classObjs = new Class>[interfaces.length];
+ for (int i = 0; i < interfaces.length; i++) {
+ Class> cl = Class.forName(interfaces[i], false, loader);
+
+ // Check if this interface is non-public (package-private)
+ if ((cl.getModifiers() & java.lang.reflect.Modifier.PUBLIC) == 0) {
+ if (hasNonPublicInterface) {
+ // Multiple non-public interfaces must be from the same classloader
+ if (nonPublicLoader != cl.getClassLoader()) {
+ throw new IllegalAccessError(
+ "conflicting non-public interface class loaders");
+ }
+ } else {
+ nonPublicLoader = cl.getClassLoader();
+ hasNonPublicInterface = true;
+ }
+ }
+ classObjs[i] = cl;
+ }
+
+ try {
+ // Use the non-public interface's classloader if needed, otherwise use provided loader
+ ClassLoader loaderToUse = hasNonPublicInterface ? nonPublicLoader : loader;
+ @SuppressWarnings("deprecation")
+ Class> proxyClass = java.lang.reflect.Proxy.getProxyClass(loaderToUse, classObjs);
+ return proxyClass;
+ } catch (IllegalArgumentException e) {
+ throw new ClassNotFoundException("Proxy class creation failed", e);
+ }
+ }
+
+ /**
+ * Gets the configured ObjectInputFilter
+ *
+ * @return the filter being used
+ */
+ public ObjectInputFilter getFilter() {
+ return filter;
+ }
+
+ /**
+ * Gets the ClassLoader being used
+ *
+ * @return the ClassLoader
+ */
+ public ClassLoader getClassLoader() {
+ return loader;
+ }
+}
diff --git a/extensions/geode-modules/src/test/java/org/apache/geode/modules/session/filter/SafeDeserializationFilterTest.java b/extensions/geode-modules/src/test/java/org/apache/geode/modules/session/filter/SafeDeserializationFilterTest.java
new file mode 100644
index 000000000000..d0ca3957f1a8
--- /dev/null
+++ b/extensions/geode-modules/src/test/java/org/apache/geode/modules/session/filter/SafeDeserializationFilterTest.java
@@ -0,0 +1,829 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.geode.modules.session.filter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ObjectInputFilter.FilterInfo;
+import java.io.ObjectInputFilter.Status;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Comprehensive unit tests for SafeDeserializationFilter.
+ *
+ *
+ *
+ *
+ *
+ * Tests use Mockito to simulate ObjectInputFilter.FilterInfo without actually deserializing
+ * dangerous objects. This allows us to test the filter logic safely without executing exploits.
+ *
+ *
+ *
+ *
+ *
+ * These tests verify protection against:
+ *
+ *
+ *
+ *
+ * {@code @SuppressWarnings({"unchecked", "rawtypes"})} is used because:
+ *
+ *
+ *
+ * @see SafeDeserializationFilter
+ * @see SecureClassLoaderObjectInputStream
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class SafeDeserializationFilterTest {
+
+ private SafeDeserializationFilter filter;
+ private FilterInfo mockFilterInfo;
+
+ @Before
+ public void setUp() {
+ filter = new SafeDeserializationFilter();
+ mockFilterInfo = mock(FilterInfo.class);
+ }
+
+ @Test
+ public void testAllowedJavaLangString() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) String.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.ALLOWED);
+ }
+
+ @Test
+ public void testAllowedInteger() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) Integer.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.ALLOWED);
+ }
+
+ /**
+ * Tests that Apache Commons Collections InvokerTransformer is blocked.
+ *
+ *
+ * InvokerTransformer is a key component in the infamous "ysoserial CommonsCollections1"
+ * exploit chain. It can invoke arbitrary methods via reflection, enabling Remote Code
+ * Execution when combined with TransformedMap or LazyMap.
+ *
+ *
+ * This class has been used in attacks against major systems including:
+ *
+ *
+ *
+ *
+ * TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl) can load
+ * arbitrary bytecode during deserialization, enabling Remote Code Execution without
+ * reflection or external dependencies.
+ *
+ *
+ * An attacker can embed malicious Java bytecode in the serialized TemplatesImpl object.
+ * During deserialization, the bytecode is loaded and executed, giving the attacker full
+ * control over the application.
+ *
+ *
+ * Deep object graphs can cause stack overflow errors during deserialization.
+ * An attacker can craft a deeply nested object structure to crash the JVM.
+ *
+ *
+ * This is sufficient for legitimate session data but prevents stack exhaustion attacks.
+ */
+ @Test
+ public void testDepthExceeded() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) String.class);
+ when(mockFilterInfo.depth()).thenReturn(51L); // > MAX_DEPTH (50)
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.REJECTED);
+ }
+
+ /**
+ * Tests that excessive object references are rejected (DoS protection).
+ *
+ *
+ * Large numbers of object references can exhaust heap memory during deserialization.
+ * An attacker can craft circular references or massive object graphs to cause OutOfMemoryError.
+ *
+ *
+ * This allows reasonable session data but prevents memory exhaustion attacks.
+ */
+ @Test
+ public void testReferencesExceeded() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) String.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(10001L); // > MAX_REFERENCES (10000)
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.REJECTED);
+ }
+
+ @Test
+ public void testArraySizeExceeded() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) byte[].class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(10001L); // > MAX_ARRAY_SIZE (10000)
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.REJECTED);
+ }
+
+ @Test
+ public void testBytesExceeded() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) String.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(10_000_001L); // > MAX_BYTES (10MB)
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.REJECTED);
+ }
+
+ @Test
+ public void testNullFilterInfo() {
+ Status status = filter.checkInput(null);
+ assertThat(status).isEqualTo(Status.REJECTED);
+ }
+
+ @Test
+ public void testCustomAllowedClass() {
+ // Use a real class name that we'll configure
+ Set
+ * UUID is extensively used in Geode for:
+ *
+ *
+ *
+ *
+ * UUID is safe because it's:
+ *
+ *
+ */
+ @Test
+ public void testAllowedUUID() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.util.UUID.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.ALLOWED);
+ }
+
+ /**
+ * Tests that Optional is allowed.
+ *
+ *
+ * Optional is used throughout Geode for:
+ *
+ *
+ *
+ *
+ * Optional is safe because it's:
+ *
+ *
+ */
+ @Test
+ public void testAllowedOptional() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.util.Optional.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.ALLOWED);
+ }
+
+ /**
+ * Tests that OptionalInt is allowed.
+ */
+ @Test
+ public void testAllowedOptionalInt() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.util.OptionalInt.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.ALLOWED);
+ }
+
+ /**
+ * Tests that OptionalLong is allowed.
+ */
+ @Test
+ public void testAllowedOptionalLong() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.util.OptionalLong.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.ALLOWED);
+ }
+
+ /**
+ * Tests that OptionalDouble is allowed.
+ */
+ @Test
+ public void testAllowedOptionalDouble() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.util.OptionalDouble.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.ALLOWED);
+ }
+
+ /**
+ * Tests that Locale is allowed.
+ *
+ *
+ * Locale is used for internationalization and localization:
+ *
+ *
+ *
+ *
+ * Locale is safe because it's:
+ *
+ *
+ */
+ @Test
+ public void testAllowedLocale() {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.util.Locale.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.ALLOWED);
+ }
+
+ /**
+ * Tests that URI is allowed.
+ *
+ *
+ * URI is used for resource identification:
+ *
+ *
+ *
+ *
+ * URI is safe because it's:
+ *
+ *
+ *
+ *
+ * This test verifies that the ALLOWED_CLASSES fast path (O(1) HashSet lookup) provides
+ * measurable performance benefits over ALLOWED_PATTERNS slow path (O(n) regex matching).
+ *
+ *
+ *
+ *
+ *
+ * This is a sanity check, not a strict benchmark. The exact speedup ratio depends on JVM,
+ * CPU, and other factors. We're just verifying that exact matching isn't accidentally slower.
+ * The test logs timing information for visibility but doesn't fail on performance regression
+ * (that would be flaky in CI environments).
+ */
+ @Test
+ public void testExactMatchIsFasterThanPatternMatch() {
+ SafeDeserializationFilter testFilter = new SafeDeserializationFilter();
+ final int warmupIterations = 1000;
+ final int testIterations = 10000;
+
+ // Warm up JVM (JIT compilation, class loading, etc.)
+ for (int i = 0; i < warmupIterations; i++) {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) String.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+ testFilter.checkInput(mockFilterInfo);
+
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.time.Instant.class);
+ testFilter.checkInput(mockFilterInfo);
+ }
+
+ // Measure exact match performance (String via ALLOWED_CLASSES)
+ when(mockFilterInfo.serialClass()).thenReturn((Class) String.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ long startExact = System.nanoTime();
+ for (int i = 0; i < testIterations; i++) {
+ Status status = testFilter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.ALLOWED);
+ }
+ long exactTime = System.nanoTime() - startExact;
+ double exactAvg = exactTime / (double) testIterations;
+
+ // Measure pattern match performance (Instant via java.time.* pattern in ALLOWED_PATTERNS)
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.time.Instant.class);
+
+ long startPattern = System.nanoTime();
+ for (int i = 0; i < testIterations; i++) {
+ Status status = testFilter.checkInput(mockFilterInfo);
+ assertThat(status).isEqualTo(Status.ALLOWED);
+ }
+ long patternTime = System.nanoTime() - startPattern;
+ double patternAvg = patternTime / (double) testIterations;
+
+ // Log results for visibility (helpful for performance monitoring)
+ System.out.println("\n=== Performance Comparison ===");
+ System.out.println(
+ String.format("Exact match (String via ALLOWED_CLASSES): %d ns total, %.2f ns avg",
+ exactTime, exactAvg));
+ System.out.println(
+ String.format("Pattern match (Instant via ALLOWED_PATTERNS): %d ns total, %.2f ns avg",
+ patternTime, patternAvg));
+ System.out
+ .println(String.format("Speedup: %.2fx faster for exact match", patternAvg / exactAvg));
+
+ // Sanity check: Exact match shouldn't be significantly slower
+ // We don't enforce strict performance ratios because that's flaky in CI
+ // Just verify that exact matching isn't accidentally slower (would indicate a bug)
+ assertThat(exactAvg)
+ .as("Exact match should not be 2x slower than pattern match (indicates optimization not working)")
+ .isLessThan(patternAvg * 2.0);
+ }
+
+ /**
+ * Tests that the ALLOWED_CLASSES exact match path works correctly.
+ *
+ *
+ * Verifies that classes explicitly in ALLOWED_CLASSES (like String, Integer, HashMap)
+ * are allowed via the fast O(1) lookup path, not falling back to pattern matching.
+ *
+ *
+ * These are the most frequently deserialized classes in sessions, so they benefit most
+ * from the optimization.
+ */
+ @Test
+ public void testAllowedClassesExactMatchPath() {
+ // Test several classes that should be in ALLOWED_CLASSES
+ Class>[] exactMatchClasses = {
+ String.class,
+ Integer.class,
+ Long.class,
+ Boolean.class,
+ java.util.HashMap.class,
+ java.util.ArrayList.class,
+ java.util.Date.class,
+ java.util.UUID.class,
+ java.util.Optional.class,
+ java.util.Locale.class,
+ java.net.URI.class
+ };
+
+ for (Class> clazz : exactMatchClasses) {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) clazz);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status)
+ .as("Expected %s to be allowed via ALLOWED_CLASSES", clazz.getName())
+ .isEqualTo(Status.ALLOWED);
+ }
+ }
+
+ /**
+ * Tests that the ALLOWED_PATTERNS regex path still works correctly.
+ *
+ *
+ * Verifies that pattern-based matching (java.time.*, Collections$*) still works correctly
+ * after refactoring. These classes should NOT be in ALLOWED_CLASSES, so they must be
+ * caught by the ALLOWED_PATTERNS regex.
+ *
+ *
+ * Ensures we didn't break pattern matching functionality during the refactoring.
+ */
+ @Test
+ public void testAllowedPatternsRegexPath() {
+ // Test classes that should match patterns, not exact matches
+ Class>[] patternMatchClasses = {
+ java.time.Instant.class, // Matches ^java\.time\..*
+ java.time.LocalDate.class, // Matches ^java\.time\..*
+ java.time.LocalDateTime.class, // Matches ^java\.time\..*
+ java.time.ZonedDateTime.class // Matches ^java\.time\..*
+ };
+
+ for (Class> clazz : patternMatchClasses) {
+ when(mockFilterInfo.serialClass()).thenReturn((Class) clazz);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status)
+ .as("Expected %s to be allowed via ALLOWED_PATTERNS regex", clazz.getName())
+ .isEqualTo(Status.ALLOWED);
+ }
+ }
+
+ /**
+ * Tests backward compatibility: custom classes work with the new structure.
+ *
+ *
+ * Verifies that custom allowed classes (added via createWithAllowedClasses) still work
+ * correctly after introducing ALLOWED_CLASSES. This ensures we didn't break the API.
+ *
+ *
+ * Users may already have code using createWithAllowedClasses() or createWithAllowedPatterns().
+ * The refactoring should be transparent to them.
+ */
+ @Test
+ public void testCustomAllowedClassesWithNewStructure() {
+ // Create filter with custom exact class
+ SafeDeserializationFilter customFilter =
+ SafeDeserializationFilter.createWithAllowedClasses(
+ "java.io.File",
+ "java.io.FileInputStream",
+ "com.example.CustomClass");
+
+ // Test that custom exact class works
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.io.File.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = customFilter.checkInput(mockFilterInfo);
+ assertThat(status)
+ .as("Custom allowed class should work with new ALLOWED_CLASSES structure")
+ .isEqualTo(Status.ALLOWED);
+ }
+
+ /**
+ * Tests backward compatibility: custom patterns work with the new structure.
+ *
+ *
+ * Verifies that custom patterns (added via createWithAllowedPatterns) still work correctly.
+ * This ensures the refactoring is backward compatible.
+ */
+ @Test
+ public void testCustomAllowedPatternsWithNewStructure() {
+ // Create filter with custom pattern
+ SafeDeserializationFilter customFilter =
+ SafeDeserializationFilter.createWithAllowedPatterns(
+ "^java\\.io\\..*",
+ "^com\\.example\\..*");
+
+ // Test that custom pattern works
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.io.File.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = customFilter.checkInput(mockFilterInfo);
+ assertThat(status)
+ .as("Custom pattern should work with new ALLOWED_PATTERNS structure")
+ .isEqualTo(Status.ALLOWED);
+ }
+
+ /**
+ * Tests that the optimization doesn't accidentally allow blocked classes.
+ *
+ *
+ * Verifies that security logic is preserved - blocked classes are still blocked even with
+ * the new ALLOWED_CLASSES structure. This is a critical security regression test.
+ *
+ *
+ * We changed internal structure but must maintain security guarantees. Blocked classes
+ * must NEVER be allowed, regardless of optimization.
+ */
+ @Test
+ public void testBlockedClassesStillBlockedAfterRefactoring() {
+ // Test that a blocked class is still blocked (security regression test)
+ // Even if someone accidentally added it to ALLOWED_CLASSES, the blocklist check comes first
+
+ // File is not blocked, but it's not in the default whitelist either
+ when(mockFilterInfo.serialClass()).thenReturn((Class) java.io.File.class);
+ when(mockFilterInfo.depth()).thenReturn(1L);
+ when(mockFilterInfo.references()).thenReturn(1L);
+ when(mockFilterInfo.arrayLength()).thenReturn(-1L);
+ when(mockFilterInfo.streamBytes()).thenReturn(100L);
+
+ Status status = filter.checkInput(mockFilterInfo);
+ assertThat(status)
+ .as("File should be rejected (not in whitelist)")
+ .isEqualTo(Status.REJECTED);
+ }
+}
diff --git a/geode-assembly/src/integrationTest/resources/assembly_content.txt b/geode-assembly/src/integrationTest/resources/assembly_content.txt
index 24a7d683b732..66d25ad44493 100644
--- a/geode-assembly/src/integrationTest/resources/assembly_content.txt
+++ b/geode-assembly/src/integrationTest/resources/assembly_content.txt
@@ -815,6 +815,7 @@ javadoc/org/apache/geode/modules/session/catalina/callback/package-summary.html
javadoc/org/apache/geode/modules/session/catalina/callback/package-tree.html
javadoc/org/apache/geode/modules/session/catalina/package-summary.html
javadoc/org/apache/geode/modules/session/catalina/package-tree.html
+javadoc/org/apache/geode/modules/session/filter/SafeDeserializationFilter.html
javadoc/org/apache/geode/modules/session/filter/SessionCachingFilter.RequestWrapper.html
javadoc/org/apache/geode/modules/session/filter/SessionCachingFilter.html
javadoc/org/apache/geode/modules/session/filter/package-summary.html
@@ -844,6 +845,7 @@ javadoc/org/apache/geode/modules/util/RegionHelper.html
javadoc/org/apache/geode/modules/util/RegionSizeFunction.html
javadoc/org/apache/geode/modules/util/RegionStatus.html
javadoc/org/apache/geode/modules/util/ResourceManagerValidator.html
+javadoc/org/apache/geode/modules/util/SecureClassLoaderObjectInputStream.html
javadoc/org/apache/geode/modules/util/SessionCustomExpiry.html
javadoc/org/apache/geode/modules/util/TouchPartitionedRegionEntriesFunction.html
javadoc/org/apache/geode/modules/util/TouchReplicatedRegionEntriesFunction.html