This is an automated email from the ASF dual-hosted git repository.

liyang-kylin pushed a commit to branch kylin5
in repository https://gitbox.apache.org/repos/asf/kylin.git

commit cbd8912e14cf5f966ffdfc3b49691d4be1f35939
Author: Li Yang <[email protected]>
AuthorDate: Mon Oct 5 13:35:24 2026 +0800

    KYLIN-6116 restrict memcached cache deserialization to allowed classes
    
    Route the memcached transcoder and cache adaptor deserialization through
    SerializeUtil's class allowlist so a poisoned memcached item cannot
    smuggle an arbitrary object graph through the serialized-flag path.
---
 .../cache/memcached/CompositeMemcachedCache.java   |   8 +-
 .../kylin/rest/cache/memcached/MemcachedCache.java |  26 ++++-
 .../cache/memcached/MemcachedChunkingCache.java    |   4 +-
 .../MemcachedCacheDeserializationTest.java         | 108 +++++++++++++++++++++
 4 files changed, 139 insertions(+), 7 deletions(-)

diff --git 
a/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/CompositeMemcachedCache.java
 
b/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/CompositeMemcachedCache.java
index 39e65ab3d2..f4e5276c10 100644
--- 
a/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/CompositeMemcachedCache.java
+++ 
b/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/CompositeMemcachedCache.java
@@ -24,11 +24,11 @@ import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 
-import org.apache.commons.lang3.SerializationUtils;
 import org.apache.kylin.common.Singletons;
 import org.apache.kylin.common.util.JsonUtil;
 import org.apache.kylin.rest.cache.KylinCache;
 import org.apache.kylin.rest.service.CommonQueryCacheSupporter;
+import org.apache.kylin.rest.util.SerializeUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.cache.Cache;
@@ -213,7 +213,7 @@ public class CompositeMemcachedCache implements KylinCache {
             if (value == null || value.length == 0) {
                 return null;
             }
-            return new 
SimpleValueWrapper(SerializationUtils.deserialize(value));
+            return new SimpleValueWrapper(SerializeUtil.deserialize(value));
         }
 
         @Override
@@ -242,7 +242,7 @@ public class CompositeMemcachedCache implements KylinCache {
             if (value == null || value.length == 0) {
                 return null;
             }
-            Object obj = SerializationUtils.deserialize(value);
+            Object obj = SerializeUtil.deserialize(value);
             if (obj != null && type != null && !type.isInstance(value)) {
                 throw new IllegalStateException(
                         "Cached value is not of required type [" + 
type.getName() + "]: " + Arrays.toString(value));
@@ -264,7 +264,7 @@ public class CompositeMemcachedCache implements KylinCache {
                 memcachedCache.put(key, value);
                 return null;
             } else {
-                return new 
SimpleValueWrapper(SerializationUtils.deserialize(existing));
+                return new 
SimpleValueWrapper(SerializeUtil.deserialize(existing));
             }
         }
     }
diff --git 
a/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/MemcachedCache.java
 
b/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/MemcachedCache.java
index 67c667b9f2..e814071f5a 100644
--- 
a/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/MemcachedCache.java
+++ 
b/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/MemcachedCache.java
@@ -44,6 +44,7 @@ import org.apache.kylin.guava30.shaded.common.base.Strings;
 import org.apache.kylin.guava30.shaded.common.base.Throwables;
 import org.apache.kylin.guava30.shaded.common.primitives.Ints;
 import org.apache.kylin.guava30.shaded.common.primitives.Shorts;
+import org.apache.kylin.rest.util.SerializeUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -116,7 +117,7 @@ public class MemcachedCache {
 
     public static MemcachedCache create(final MemcachedCacheConfig config, 
String memcachedPrefix, int timeToLive) {
         try {
-            SerializingTranscoder transcoder = new 
SerializingTranscoder(config.getMaxObjectSize());
+            SerializingTranscoder transcoder = new 
KylinSerializingTranscoder(config.getMaxObjectSize());
             // always no compression inside, we compress/decompress outside
             transcoder.setCompressionThreshold(Integer.MAX_VALUE);
 
@@ -388,4 +389,27 @@ public class MemcachedCache {
 
     }
 
+    /**
+     * The client only ever stores byte[] values produced by encodeValue, but 
a poisoned
+     * memcached item can set the serialized flag and smuggle an arbitrary 
object graph
+     * through the default transcoder. Route transcoder deserialization 
through the same
+     * class allowlist used by SerializeUtil so disallowed classes are 
rejected before they
+     * are instantiated.
+     */
+    @VisibleForTesting
+    static final class KylinSerializingTranscoder extends 
SerializingTranscoder {
+
+        KylinSerializingTranscoder(int maxSize) {
+            super(maxSize);
+        }
+
+        @Override
+        protected Object deserialize(byte[] in) {
+            if (in == null) {
+                return null;
+            }
+            return SerializeUtil.deserialize(in);
+        }
+    }
+
 }
diff --git 
a/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/MemcachedChunkingCache.java
 
b/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/MemcachedChunkingCache.java
index 175d38e9cd..857be64a95 100644
--- 
a/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/MemcachedChunkingCache.java
+++ 
b/src/common-service/src/main/java/org/apache/kylin/rest/cache/memcached/MemcachedChunkingCache.java
@@ -27,7 +27,6 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
-import org.apache.commons.lang3.SerializationUtils;
 import org.apache.kylin.common.util.Pair;
 import org.apache.kylin.guava30.shaded.common.base.Preconditions;
 import org.apache.kylin.guava30.shaded.common.base.Strings;
@@ -35,6 +34,7 @@ import org.apache.kylin.guava30.shaded.common.base.Throwables;
 import org.apache.kylin.guava30.shaded.common.collect.Maps;
 import org.apache.kylin.guava30.shaded.common.primitives.Ints;
 import org.apache.kylin.guava30.shaded.common.primitives.Shorts;
+import org.apache.kylin.rest.util.SerializeUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -267,6 +267,6 @@ public class MemcachedChunkingCache extends MemcachedCache 
implements KeyHookLoo
         if (bytes == null || bytes.length == 0) {
             return null;
         }
-        return SerializationUtils.deserialize(bytes);
+        return (KeyHook) SerializeUtil.deserialize(bytes);
     }
 }
diff --git 
a/src/common-service/src/test/java/org/apache/kylin/rest/cache/memcached/MemcachedCacheDeserializationTest.java
 
b/src/common-service/src/test/java/org/apache/kylin/rest/cache/memcached/MemcachedCacheDeserializationTest.java
new file mode 100755
index 0000000000..4ad078662b
--- /dev/null
+++ 
b/src/common-service/src/test/java/org/apache/kylin/rest/cache/memcached/MemcachedCacheDeserializationTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.kylin.rest.cache.memcached;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.SerializationUtils;
+import org.apache.kylin.common.util.NLocalFileMetadataTestCase;
+import 
org.apache.kylin.rest.cache.memcached.CompositeMemcachedCache.MemCachedCacheAdaptor;
+import org.apache.kylin.rest.service.CommonQueryCacheSupporter;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import net.spy.memcached.CachedData;
+import net.spy.memcached.MemcachedClient;
+import net.spy.memcached.internal.GetFuture;
+
+public class MemcachedCacheDeserializationTest extends 
NLocalFileMetadataTestCase {
+
+    private static final int MAX_OBJECT_SIZE = 1024 * 1024;
+
+    private MemcachedClient memcachedClient;
+    private MemcachedCache memcachedCache;
+    private MemCachedCacheAdaptor adaptor;
+
+    @Before
+    public void setUp() throws Exception {
+        createTestMetadata();
+        MemcachedCacheConfig cacheConfig = new MemcachedCacheConfig();
+        cacheConfig.setMaxObjectSize(MAX_OBJECT_SIZE);
+        memcachedClient = mock(MemcachedClient.class);
+        memcachedCache = new MemcachedCache(memcachedClient, cacheConfig,
+                
CommonQueryCacheSupporter.Type.SUCCESS_QUERY_CACHE.rootCacheName, 7 * 24 * 
3600);
+        adaptor = new MemCachedCacheAdaptor(new 
MemcachedChunkingCache(memcachedCache));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        cleanupTestMetadata();
+    }
+
+    private void mockCachedValue(String keyS, byte[] encodedValue) throws 
Exception {
+        GetFuture<Object> future = mock(GetFuture.class);
+        
when(memcachedClient.asyncGet(memcachedCache.computeKeyHash(keyS))).thenReturn(future);
+        when(future.get(anyLong(), 
any(TimeUnit.class))).thenReturn(encodedValue);
+    }
+
+    @Test
+    public void testAllowedValueRoundTrip() throws Exception {
+        String keyS = "allowed-key";
+        byte[] valueBytes = SerializationUtils.serialize("cached-value");
+        KeyHookLookup.KeyHook keyHook = new KeyHookLookup.KeyHook(null, 
valueBytes);
+        mockCachedValue(keyS, memcachedCache.encodeValue(keyS, keyHook));
+
+        Assert.assertEquals("cached-value", adaptor.get(keyS).get());
+    }
+
+    @Test
+    public void testRejectDisallowedValueClass() throws Exception {
+        String keyS = "poisoned-value";
+        byte[] valueBytes = SerializationUtils.serialize(new 
File("/tmp/kylin-gadget"));
+        KeyHookLookup.KeyHook keyHook = new KeyHookLookup.KeyHook(null, 
valueBytes);
+        mockCachedValue(keyS, memcachedCache.encodeValue(keyS, keyHook));
+
+        Assert.assertThrows(IllegalStateException.class, () -> 
adaptor.get(keyS));
+    }
+
+    @Test
+    public void testRejectDisallowedKeyHookClass() throws Exception {
+        String keyS = "poisoned-keyhook";
+        mockCachedValue(keyS, memcachedCache.encodeValue(keyS, new 
File("/tmp/kylin-gadget")));
+
+        Assert.assertThrows(IllegalStateException.class, () -> 
adaptor.get(keyS));
+    }
+
+    @Test
+    public void testTranscoderRejectsDisallowedSerializedPayload() {
+        MemcachedCache.KylinSerializingTranscoder transcoder = new 
MemcachedCache.KylinSerializingTranscoder(
+                MAX_OBJECT_SIZE);
+
+        CachedData poisoned = transcoder.encode(new File("/tmp/kylin-gadget"));
+        Assert.assertThrows(IllegalStateException.class, () -> 
transcoder.decode(poisoned));
+    }
+}

Reply via email to