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 b3ded9e8ac35f95a799871c87d248603fb4f994b Author: Li Yang <[email protected]> AuthorDate: Mon Oct 5 10:04:08 2026 +0800 KYLIN-6116 restrict cache deserialization to allowed classes --- .../org/apache/kylin/rest/util/SerializeUtil.java | 73 +++++++++++++++++- .../apache/kylin/rest/util/SerializeUtilTest.java | 90 ++++++++++++++++++++++ 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/src/core-common/src/main/java/org/apache/kylin/rest/util/SerializeUtil.java b/src/core-common/src/main/java/org/apache/kylin/rest/util/SerializeUtil.java index 45bc59b3ed..a7516f8bd3 100644 --- a/src/core-common/src/main/java/org/apache/kylin/rest/util/SerializeUtil.java +++ b/src/core-common/src/main/java/org/apache/kylin/rest/util/SerializeUtil.java @@ -20,13 +20,33 @@ package org.apache.kylin.rest.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidClassException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.util.Arrays; +import java.util.List; import lombok.experimental.UtilityClass; @UtilityClass public class SerializeUtil { + + /** + * Packages whose classes are allowed to appear in a serialized cache value. Any other class is + * rejected while the stream is being resolved, before it can be instantiated. This blocks the + * third-party gadget chains (commons-collections, the JDK internal XML/management classes, and + * so on) that a poisoned cache entry would otherwise rely on. + */ + private static final List<String> ALLOWED_CLASS_PREFIXES = Arrays.asList( // + "org.apache.kylin.", // + "java.lang.", // + "java.util.", // + "java.math.", // + "java.time."); + public static byte[] serialize(Object object) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { @@ -38,11 +58,60 @@ public class SerializeUtil { } public static Object deserialize(byte[] bytes) { - - try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { + try (ObjectInputStream ois = new FilteringObjectInputStream(new ByteArrayInputStream(bytes))) { return ois.readObject(); } catch (Exception e) { throw new IllegalStateException("deserialize failed", e); } } + + static boolean isAllowedClassName(String className) { + String component = className; + boolean array = false; + while (component.startsWith("[")) { + component = component.substring(1); + array = true; + } + if (array) { + if (component.length() == 1 && "BCDFIJSZ".indexOf(component.charAt(0)) >= 0) { + return true; + } + if (component.startsWith("L") && component.endsWith(";")) { + component = component.substring(1, component.length() - 1); + } + } + for (String prefix : ALLOWED_CLASS_PREFIXES) { + if (component.startsWith(prefix)) { + return true; + } + } + return false; + } + + static final class FilteringObjectInputStream extends ObjectInputStream { + + FilteringObjectInputStream(InputStream inputStream) throws IOException { + super(inputStream); + } + + @Override + protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { + String className = desc.getName(); + if (!isAllowedClassName(className)) { + throw new InvalidClassException(className, "class is not allowed for Kylin cache deserialization"); + } + return super.resolveClass(desc); + } + + @Override + protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException { + for (String interfaceName : interfaces) { + if (!isAllowedClassName(interfaceName)) { + throw new InvalidClassException(interfaceName, + "proxy interface is not allowed for Kylin cache deserialization"); + } + } + return super.resolveProxyClass(interfaces); + } + } } diff --git a/src/core-common/src/test/java/org/apache/kylin/rest/util/SerializeUtilTest.java b/src/core-common/src/test/java/org/apache/kylin/rest/util/SerializeUtilTest.java new file mode 100644 index 0000000000..9af88618f2 --- /dev/null +++ b/src/core-common/src/test/java/org/apache/kylin/rest/util/SerializeUtilTest.java @@ -0,0 +1,90 @@ +/* + * 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.util; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.InvalidClassException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.kylin.common.NativeQueryRealization; +import org.junit.jupiter.api.Test; + +class SerializeUtilTest { + + @Test + void testRoundTripAllowedCollections() { + List<String> list = Arrays.asList("kylin", "cache"); + assertEquals(list, SerializeUtil.deserialize(SerializeUtil.serialize(list))); + + Map<String, Long> map = new HashMap<>(); + map.put("scanRows", 42L); + assertEquals(map, SerializeUtil.deserialize(SerializeUtil.serialize(map))); + } + + @Test + void testRoundTripAllowedKylinType() { + NativeQueryRealization realization = new NativeQueryRealization("model-id", 7L, "table"); + Object deserialized = SerializeUtil.deserialize(SerializeUtil.serialize(realization)); + assertTrue(deserialized instanceof NativeQueryRealization); + assertEquals("model-id", ((NativeQueryRealization) deserialized).getModelId()); + } + + @Test + void testRoundTripArrays() { + int[] ints = { 1, 2, 3 }; + assertArrayEquals(ints, (int[]) SerializeUtil.deserialize(SerializeUtil.serialize(ints))); + + String[] strings = { "kylin" }; + assertArrayEquals(strings, (String[]) SerializeUtil.deserialize(SerializeUtil.serialize(strings))); + } + + @Test + void testRejectClassOutsideAllowList() { + byte[] bytes = SerializeUtil.serialize(new File("/tmp/kylin")); + IllegalStateException error = assertThrows(IllegalStateException.class, () -> SerializeUtil.deserialize(bytes)); + assertTrue(error.getCause() instanceof InvalidClassException); + } + + @Test + void testAllowedClassNames() { + assertTrue(SerializeUtil.isAllowedClassName("org.apache.kylin.rest.response.SQLResponse")); + assertTrue(SerializeUtil.isAllowedClassName("java.util.ArrayList")); + assertTrue(SerializeUtil.isAllowedClassName("java.lang.String")); + assertTrue(SerializeUtil.isAllowedClassName("java.math.BigDecimal")); + assertTrue(SerializeUtil.isAllowedClassName("[B")); + assertTrue(SerializeUtil.isAllowedClassName("[[Ljava.lang.String;")); + } + + @Test + void testRejectedClassNames() { + assertFalse(SerializeUtil.isAllowedClassName("java.io.File")); + assertFalse(SerializeUtil.isAllowedClassName("org.apache.commons.collections.functors.InvokerTransformer")); + assertFalse(SerializeUtil.isAllowedClassName("javax.management.BadAttributeValueExpException")); + assertFalse(SerializeUtil.isAllowedClassName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl")); + } +}
