diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/SerializationUtilities.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/SerializationUtilities.java index b61f4d484439..388ab1d913b0 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/SerializationUtilities.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/SerializationUtilities.java @@ -32,12 +32,14 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -58,6 +60,7 @@ import org.apache.hadoop.hive.ql.plan.MapredWork; import org.apache.hadoop.hive.ql.plan.PartitionDesc; import org.apache.hadoop.hive.ql.session.SessionState; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDF; import org.apache.hadoop.hive.serde2.Serializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -126,8 +129,10 @@ private static class KryoWithHooks extends Kryo implements Configurable { private Hook globalHook; // this should be set on-the-fly after borrowing this instance and needs to be reset on release private Configuration configuration; - // default false, should be reset on release - private boolean isExprNodeFirst = false; + // when non-null, the stream being deserialized is untrusted: the first class read must be + // compatible with this type and every class read must pass + // isAllowedForUntrustedDeserialization(); default null (trusted), reset on release + private Class untrustedRootType = null; // total classes we have met during (de)serialization, should be reset on release private long classCounter = 0; @@ -237,13 +242,20 @@ public Configuration getConf() { @Override public com.esotericsoftware.kryo.kryo5.Registration getRegistration(Class type) { - // If PartitionExpressionForMetastore performs deserialization at remote HMS, - // the first class encountered during deserialization must be an ExprNodeDesc, - // throw exception to avoid potential security problem if it is not. - if (isExprNodeFirst && classCounter == 0) { - if (!ExprNodeDesc.class.isAssignableFrom(type)) { + // If this instance deserializes a payload that a remote client controls (e.g. PartitionExpressionForMetastore at + // a remote HMS) or that a client can persist (e.g. a table property copied into the job conf), the first class + // encountered during deserialization must be compatible with the expected root type, and every class in the + // stream must pass the allowlist check. Kryo is otherwise willing to instantiate any classpath class named by the + // payload (registrationRequired=false plus StdInstantiatorStrategy), which turns these payloads into a + // deserialization-of-untrusted-data primitive. + if (untrustedRootType != null) { + if (classCounter == 0 && !untrustedRootType.isAssignableFrom(type)) { + throw new UnsupportedOperationException("The object to be deserialized must be a " + + untrustedRootType.getName() + ", but encountered: " + type); + } + if (!isAllowedForUntrustedDeserialization(type)) { throw new UnsupportedOperationException( - "The object to be deserialized must be an ExprNodeDesc, but encountered: " + type); + "Deserialization of " + type + " is not allowed from an untrusted payload"); } } classCounter++; @@ -251,17 +263,81 @@ public com.esotericsoftware.kryo.kryo5.Registration getRegistration(Class type) } public void setExprNodeFirst(boolean isPartFilter) { - this.isExprNodeFirst = isPartFilter; + setUntrustedRootType(isPartFilter ? ExprNodeDesc.class : null); + } + + void setUntrustedRootType(Class rootType) { + this.untrustedRootType = rootType; + this.classCounter = 0; } // reset the fields on release public void restore() { setConf(null); - isExprNodeFirst = false; + untrustedRootType = null; classCounter = 0; } } + /** + * Package prefixes that classes read from an untrusted Kryo payload may come from. These cover everything a + * legitimate serialized expression ({@link ExprNodeDesc} graph) or search argument (SearchArgumentImpl graph) + * contains: expression descriptors and plan literals, builtin and installed UDFs, type infos and object inspectors, + * Hive/Hadoop value types, and plain JDK value/collection classes. Known gadget carriers (commons-collections, + * beanutils, xalan/TemplatesImpl, ...) all live outside these prefixes. + */ + private static final String[] UNTRUSTED_ALLOWED_PACKAGE_PREFIXES = new String[] { + "java.lang.", + "java.util.", + "java.sql.", + "java.time.", + "java.math.", + "org.apache.hadoop.hive.ql.plan.", + "org.apache.hadoop.hive.ql.udf.", + "org.apache.hadoop.hive.ql.io.sarg.", + "org.apache.hadoop.hive.serde2.", + "org.apache.hadoop.hive.common.type.", + "org.apache.hadoop.io." + }; + + /** + * Classes that are never acceptable in an untrusted payload even though they pass the package allowlist. + * GenericUDFReflect, GenericUDFReflect2, and GenericUDFInFile are typically disallowed in a secure environment. + * {@link org.apache.hadoop.hive.ql.security.authorization.plugin.SettableConfigUpdater} + */ + private static final Set UNTRUSTED_DENIED_CLASS_NAMES = new HashSet<>(Arrays.asList( + "org.apache.hadoop.hive.ql.udf.generic.GenericUDFReflect", + "org.apache.hadoop.hive.ql.udf.generic.GenericUDFReflect2", + "org.apache.hadoop.hive.ql.udf.generic.GenericUDFInFile" + )); + + @VisibleForTesting + static boolean isAllowedForUntrustedDeserialization(Class type) { + Class component = type; + while (component.isArray()) { + component = component.getComponentType(); + } + if (component.isPrimitive()) { + return true; + } + String name = component.getName(); + if (UNTRUSTED_DENIED_CLASS_NAMES.contains(name)) { + return false; + } + // Custom (temporary/permanent) UDFs live in user packages. The classes themselves were + // installed by an administrator, so allowing kryo to instantiate them is no worse than any + // query invoking them. + if (GenericUDF.class.isAssignableFrom(component) || UDF.class.isAssignableFrom(component)) { + return true; + } + for (String prefix : UNTRUSTED_ALLOWED_PACKAGE_PREFIXES) { + if (name.startsWith(prefix)) { + return true; + } + } + return false; + } + private static final Object FAKE_REFERENCE = new Object(); // Bounded queue could be specified here but that will lead to blocking. @@ -883,7 +959,29 @@ public static String serializeExpression(ExprNodeGenericFuncDesc expr) { public static ExprNodeGenericFuncDesc deserializeExpression(String s) { byte[] bytes = Base64.decodeBase64(s.getBytes(StandardCharsets.UTF_8)); - return deserializeObjectFromKryo(bytes, ExprNodeGenericFuncDesc.class); + // Serialized expressions travel through configuration values (e.g. + // hive.io.filter.expr.serialized) that clients can shadow via table properties or SET, so + // they must always be deserialized with the untrusted-payload restrictions. + return deserializeUntrustedObjectFromKryo(bytes, ExprNodeGenericFuncDesc.class); + } + + /** + * Deserializes bytes that a client may control (a remote-supplied expression, a value read back + * from a configuration key that table properties can shadow, ...). In addition to pinning the + * root object to {@code clazz}, every class named in the stream is validated against a fixed + * allowlist, so the payload cannot make Kryo instantiate arbitrary classpath classes. + * @param bytes Bytes containing the object. + * @param clazz The expected class of the root object. + * @return The deserialized object. + */ + public static T deserializeUntrustedObjectFromKryo(byte[] bytes, Class clazz) { + KryoWithHooks kryo = (KryoWithHooks) borrowKryo(); + kryo.setUntrustedRootType(clazz); + try (Input inp = new Input(new ByteArrayInputStream(bytes))) { + return kryo.readObject(inp, clazz); + } finally { + releaseKryo(kryo); + } } public static byte[] serializeObjectToKryo(Serializable object) { diff --git a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/ppr/PartitionExpressionForMetastore.java b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/ppr/PartitionExpressionForMetastore.java index 522c9896684f..0c834f35a2a6 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/ppr/PartitionExpressionForMetastore.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/ppr/PartitionExpressionForMetastore.java @@ -30,15 +30,19 @@ import org.apache.hadoop.hive.metastore.PartitionExpressionProxy; import org.apache.hadoop.hive.metastore.api.MetaException; import org.apache.hadoop.hive.ql.exec.SerializationUtilities; +import org.apache.hadoop.hive.ql.exec.UDF; import org.apache.hadoop.hive.ql.io.orc.OrcFileFormatProxy; import org.apache.hadoop.hive.ql.io.orc.OrcInputFormat; -import org.apache.hadoop.hive.ql.io.sarg.ConvertAstToSearchArg; import org.apache.hadoop.hive.ql.io.sarg.SearchArgument; +import org.apache.hadoop.hive.ql.io.sarg.SearchArgumentImpl; import org.apache.hadoop.hive.ql.metadata.HiveException; import org.apache.hadoop.hive.ql.parse.SemanticException; import org.apache.hadoop.hive.ql.plan.ExprNodeDesc; import org.apache.hadoop.hive.ql.plan.ExprNodeDescUtils; import org.apache.hadoop.hive.ql.plan.ExprNodeGenericFuncDesc; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDF; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDFBridge; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDFMacro; import org.apache.hadoop.hive.serde2.typeinfo.PrimitiveTypeInfo; import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoFactory; import org.slf4j.Logger; @@ -111,21 +115,64 @@ private ExprNodeDesc deserializeExpr(byte[] exprBytes) throws MetaException { try { expr = SerializationUtilities.deserializeObjectWithTypeInformation(exprBytes, true); } catch (Exception ex) { - LOG.error("Failed to deserialize the expression, fall back to deserializeObjectFromKryo", ex); + LOG.error("Failed to deserialize the expression, fall back to deserializeUntrustedObjectFromKryo", ex); try { - expr = SerializationUtilities.deserializeObjectFromKryo(exprBytes, ExprNodeGenericFuncDesc.class); + // The fallback must use the same untrusted-payload restrictions as the primary path: these bytes come straight + // from a Thrift client. + expr = SerializationUtilities.deserializeUntrustedObjectFromKryo(exprBytes, ExprNodeGenericFuncDesc.class); } catch (Exception e) { LOG.error("Failed to deserialize the expression", e); throw new MetaException("SerializationUtilities#deserializeObjectWithTypeInformation: " + ex.getMessage() + - ", SerializationUtilities#deserializeObjectFromKryo: " + e.getMessage()); + ", SerializationUtilities#deserializeUntrustedObjectFromKryo: " + e.getMessage()); } } if (expr == null) { throw new MetaException("Failed to deserialize expression - ExprNodeDesc not present"); } + validateDeserializedExpr(expr); return expr; } + /** + * Rejects client-supplied expression graphs that would execute arbitrary code when the metastore stringifies or + * evaluates them. The Kryo-level class allowlist already blocks reflect/reflect2/java_method/in_file; a + * {@link GenericUDFBridge} instance is legitimate (it wraps builtin old-style UDFs like year()), but it instantiates + * whatever class name its {@code udfClassName} field carries, so that name must resolve to a real {@link UDF}. + */ + private void validateDeserializedExpr(ExprNodeDesc expr) throws MetaException { + if (expr instanceof ExprNodeGenericFuncDesc exprNodeGenericFuncDesc) { + validateDeserializedExprNodeGenericFuncDesc(exprNodeGenericFuncDesc); + } + if (expr.getChildren() != null) { + for (ExprNodeDesc child : expr.getChildren()) { + validateDeserializedExpr(child); + } + } + } + + private void validateDeserializedExprNodeGenericFuncDesc(ExprNodeGenericFuncDesc expr) throws MetaException { + GenericUDF genericUDF = expr.getGenericUDF(); + if (genericUDF instanceof GenericUDFBridge genericUDFBridge) { + String udfClassName = genericUDFBridge.getUdfClassName(); + Class udfClass; + try { + udfClass = Class.forName(udfClassName, false, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException | LinkageError e) { + throw new MetaException("Unknown UDF class in partition filter expression: " + udfClassName); + } + if (!UDF.class.isAssignableFrom(udfClass)) { + throw new MetaException("Class in partition filter expression is not a UDF: " + udfClassName); + } + } + if (genericUDF instanceof GenericUDFMacro genericUDFMacro) { + // a macro body is an expression graph of its own + ExprNodeDesc body = genericUDFMacro.getBody(); + if (body != null) { + validateDeserializedExpr(body); + } + } + } + @Override public FileFormatProxy getFileFormatProxy(FileMetadataExprType type) { switch (type) { @@ -150,6 +197,8 @@ public FileMetadataExprType getMetadataType(String inputFormat) { @Override public SearchArgument createSarg(byte[] expr) { - return ConvertAstToSearchArg.create(expr); + // These bytes also come straight from a Thrift client (get_file_metadata_by_expr), so they + // get the same untrusted-payload restrictions as the partition filter expressions above. + return SerializationUtilities.deserializeUntrustedObjectFromKryo(expr, SearchArgumentImpl.class); } } diff --git a/ql/src/test/org/apache/hadoop/hive/metastore/TestMetastoreExpr.java b/ql/src/test/org/apache/hadoop/hive/metastore/TestMetastoreExpr.java index fab397c52063..22909e4407cd 100644 --- a/ql/src/test/org/apache/hadoop/hive/metastore/TestMetastoreExpr.java +++ b/ql/src/test/org/apache/hadoop/hive/metastore/TestMetastoreExpr.java @@ -55,7 +55,7 @@ import com.google.common.collect.Lists; - +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assert.assertEquals; import org.junit.Before; @@ -170,6 +170,18 @@ public void testPartitionExpr() throws Exception { } catch (IMetaStoreClient.IncompatibleMetastoreException ignore) { } + // Denied expression => throw the specific exception + try { + var expr = e.val("currentTimeMillis").val("java.lang.System").fn("reflect", TypeInfoFactory.intTypeInfo, 2).val(0) + .pred("=", 2).build(); + checkExpr(-1, dbName, tblName, expr, tbl); + fail("Should have thrown"); + } catch (IMetaStoreClient.IncompatibleMetastoreException ex) { + assertTrue(ex.getMessage().startsWith("SerializationUtilities#deserializeObjectWithTypeInformation: " + + "java.lang.UnsupportedOperationException: Deserialization of " + + "class org.apache.hadoop.hive.ql.udf.generic.GenericUDFReflect is not allowed from an untrusted payload")); + } + // Invalid expression => throw some exception, but not incompatible metastore. try { checkExpr(-1, dbName, tblName, e.val(31).intCol("p3").pred(">", 2).build(), tbl); diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/TestSerializationUtilities.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/TestSerializationUtilities.java index 0c003d5e46de..dc2033f6a76d 100644 --- a/ql/src/test/org/apache/hadoop/hive/ql/exec/TestSerializationUtilities.java +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/TestSerializationUtilities.java @@ -31,6 +31,7 @@ import java.util.Optional; import java.util.Properties; +import com.esotericsoftware.kryo.kryo5.KryoException; import com.google.common.collect.ArrayListMultimap; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; @@ -46,7 +47,11 @@ import org.apache.hadoop.hive.ql.plan.PartitionDesc; import org.apache.hadoop.hive.ql.plan.TableDesc; import org.apache.hadoop.hive.ql.plan.VectorPartitionDesc; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDFInFile; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDFOPEqual; import org.apache.hadoop.hive.ql.udf.generic.GenericUDFOPNull; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDFReflect; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDFReflect2; import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoFactory; import org.junit.Assert; import org.junit.Test; @@ -246,4 +251,90 @@ private static MapWork mockMapWorkWithSomePartitionDescProperties() throws Excep return mapWork; } + + @Test + public void testUntrustedDeserializationAcceptsLegitimateExpression() { + ExprNodeGenericFuncDesc expr = buildColumnEqualsConstant(new ExprNodeConstantDesc( + TypeInfoFactory.stringTypeInfo, "value")); + + byte[] typed = SerializationUtilities.serializeObjectWithTypeInformation(expr); + Object deserialized = SerializationUtilities.deserializeObjectWithTypeInformation(typed, true); + Assert.assertTrue(deserialized instanceof ExprNodeGenericFuncDesc); + + String base64 = SerializationUtilities.serializeExpression(expr); + Assert.assertNotNull(SerializationUtilities.deserializeExpression(base64)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testUntrustedDeserializationRejectsNonExprRoot() { + byte[] bytes = SerializationUtilities.serializeObjectWithTypeInformation( + new LinkedHashMap()); + SerializationUtilities.deserializeObjectWithTypeInformation(bytes, true); + } + + @Test(expected = KryoException.class) + public void testUntrustedDeserializationRejectsSmuggledClass() { + // a class outside the allowlist carried in a "constant" stands in for a gadget object + ExprNodeGenericFuncDesc expr = buildColumnEqualsConstant(new ExprNodeConstantDesc( + TypeInfoFactory.stringTypeInfo, new java.io.File("/tmp/x"))); + byte[] bytes = SerializationUtilities.serializeObjectWithTypeInformation(expr); + SerializationUtilities.deserializeObjectWithTypeInformation(bytes, true); + } + + @Test(expected = KryoException.class) + public void testDeserializeExpressionRejectsSmuggledClass() { + ExprNodeGenericFuncDesc expr = buildColumnEqualsConstant(new ExprNodeConstantDesc( + TypeInfoFactory.stringTypeInfo, new java.io.File("/tmp/x"))); + SerializationUtilities.deserializeExpression(SerializationUtilities.serializeExpression(expr)); + } + + @Test(expected = KryoException.class) + public void testUntrustedDeserializationRejectsReflectUdf() { + List children = new ArrayList<>(); + children.add(new ExprNodeConstantDesc(TypeInfoFactory.stringTypeInfo, "java.lang.ProcessBuilder")); + ExprNodeGenericFuncDesc expr = new ExprNodeGenericFuncDesc(TypeInfoFactory.stringTypeInfo, + new GenericUDFReflect(), children); + byte[] bytes = SerializationUtilities.serializeObjectWithTypeInformation(expr); + SerializationUtilities.deserializeObjectWithTypeInformation(bytes, true); + } + + @Test(expected = KryoException.class) + public void testUntrustedDeserializationRejectsReflect2Udf() { + List children = new ArrayList<>(); + children.add(new ExprNodeConstantDesc(TypeInfoFactory.stringTypeInfo, "java.lang.ProcessBuilder")); + ExprNodeGenericFuncDesc expr = new ExprNodeGenericFuncDesc(TypeInfoFactory.stringTypeInfo, + new GenericUDFReflect2(), children); + byte[] bytes = SerializationUtilities.serializeObjectWithTypeInformation(expr); + SerializationUtilities.deserializeObjectWithTypeInformation(bytes, true); + } + + @Test(expected = KryoException.class) + public void testUntrustedDeserializationRejectsInFileUdf() { + List children = new ArrayList<>(); + children.add(new ExprNodeConstantDesc(TypeInfoFactory.stringTypeInfo, "java.lang.ProcessBuilder")); + ExprNodeGenericFuncDesc expr = new ExprNodeGenericFuncDesc(TypeInfoFactory.stringTypeInfo, + new GenericUDFInFile(), children); + byte[] bytes = SerializationUtilities.serializeObjectWithTypeInformation(expr); + SerializationUtilities.deserializeObjectWithTypeInformation(bytes, true); + } + + @Test + public void testUntrustedDeserializationAllowlist() { + Assert.assertTrue( + SerializationUtilities.isAllowedForUntrustedDeserialization(ExprNodeGenericFuncDesc.class)); + Assert.assertTrue(SerializationUtilities.isAllowedForUntrustedDeserialization(GenericUDFOPNull.class)); + Assert.assertTrue(SerializationUtilities.isAllowedForUntrustedDeserialization(ArrayList.class)); + Assert.assertTrue(SerializationUtilities.isAllowedForUntrustedDeserialization(byte[].class)); + Assert.assertFalse(SerializationUtilities.isAllowedForUntrustedDeserialization(java.io.File.class)); + Assert.assertFalse(SerializationUtilities.isAllowedForUntrustedDeserialization(Path.class)); + Assert.assertFalse(SerializationUtilities.isAllowedForUntrustedDeserialization(GenericUDFReflect.class)); + } + + private static ExprNodeGenericFuncDesc buildColumnEqualsConstant(ExprNodeConstantDesc constant) { + List children = new ArrayList<>(); + children.add(new ExprNodeColumnDesc(TypeInfoFactory.stringTypeInfo, "col1", "tab", false)); + children.add(constant); + return new ExprNodeGenericFuncDesc(TypeInfoFactory.booleanTypeInfo, + new GenericUDFOPEqual(), children); + } } diff --git a/ql/src/test/org/apache/hadoop/hive/ql/optimizer/ppr/TestPartitionExpressionForMetastore.java b/ql/src/test/org/apache/hadoop/hive/ql/optimizer/ppr/TestPartitionExpressionForMetastore.java new file mode 100644 index 000000000000..21358bb08170 --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/optimizer/ppr/TestPartitionExpressionForMetastore.java @@ -0,0 +1,85 @@ +/* + * 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.hadoop.hive.ql.optimizer.ppr; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.hadoop.hive.metastore.api.MetaException; +import org.apache.hadoop.hive.ql.exec.SerializationUtilities; +import org.apache.hadoop.hive.ql.plan.ExprNodeColumnDesc; +import org.apache.hadoop.hive.ql.plan.ExprNodeConstantDesc; +import org.apache.hadoop.hive.ql.plan.ExprNodeDesc; +import org.apache.hadoop.hive.ql.plan.ExprNodeGenericFuncDesc; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDF; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDFBridge; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDFOPEqual; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDFReflect; +import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoFactory; +import org.junit.Assert; +import org.junit.Test; + +/** + * Verifies that the metastore-side expression deserialization only accepts benign expression + * graphs: the expression bytes arrive straight from Thrift clients, so classes outside the + * allowlist, reflect()/reflect2(), and GenericUDFBridge instances pointing at non-UDF classes + * must all be rejected before anything stringifies or evaluates the expression. + */ +public class TestPartitionExpressionForMetastore { + + @Test + public void testComparisonExpressionIsAccepted() throws Exception { + ExprNodeGenericFuncDesc expr = buildExpression(new GenericUDFOPEqual(), + new ExprNodeConstantDesc(TypeInfoFactory.stringTypeInfo, "2026-08-11")); + String filter = new PartitionExpressionForMetastore().convertExprToFilter( + SerializationUtilities.serializeObjectWithTypeInformation(expr), null, false); + Assert.assertNotNull(filter); + } + + @Test(expected = MetaException.class) + public void testReflectUdfIsRejected() throws Exception { + ExprNodeGenericFuncDesc expr = buildExpression(new GenericUDFReflect(), + new ExprNodeConstantDesc(TypeInfoFactory.stringTypeInfo, "java.lang.ProcessBuilder")); + new PartitionExpressionForMetastore().convertExprToFilter( + SerializationUtilities.serializeObjectWithTypeInformation(expr), null, false); + } + + @Test(expected = MetaException.class) + public void testBridgeToNonUdfClassIsRejected() throws Exception { + GenericUDFBridge bridge = new GenericUDFBridge("evil", false, "java.lang.ProcessBuilder"); + ExprNodeGenericFuncDesc expr = buildExpression(bridge, + new ExprNodeConstantDesc(TypeInfoFactory.stringTypeInfo, "x")); + new PartitionExpressionForMetastore().convertExprToFilter( + SerializationUtilities.serializeObjectWithTypeInformation(expr), null, false); + } + + @Test(expected = MetaException.class) + public void testSmuggledClassIsRejected() throws Exception { + ExprNodeGenericFuncDesc expr = buildExpression(new GenericUDFOPEqual(), + new ExprNodeConstantDesc(TypeInfoFactory.stringTypeInfo, new java.io.File("/tmp/x"))); + new PartitionExpressionForMetastore().convertExprToFilter( + SerializationUtilities.serializeObjectWithTypeInformation(expr), null, false); + } + + private ExprNodeGenericFuncDesc buildExpression(GenericUDF udf, ExprNodeConstantDesc constant) { + List children = new ArrayList<>(); + children.add(new ExprNodeColumnDesc(TypeInfoFactory.stringTypeInfo, "ds", "tab", true)); + children.add(constant); + return new ExprNodeGenericFuncDesc(TypeInfoFactory.booleanTypeInfo, udf, children); + } +}