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)); + } +}
