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

dongjoon-hyun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new 321e051738bb [SPARK-57448][SQL] Support `spark.sql.reflect.allowList` 
to restrict methods invoked by `reflect/java_method`
321e051738bb is described below

commit 321e051738bb7c2d1d719124ca28015329e31346
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Sun Jun 14 23:08:58 2026 -0700

    [SPARK-57448][SQL] Support `spark.sql.reflect.allowList` to restrict 
methods invoked by `reflect/java_method`
    
    ### What changes were proposed in this pull request?
    
    For Apache Spark 4.3.0, this PR aims to add a static SQL config 
`spark.sql.reflect.allowList` that restricts which static methods the 
`reflect`/`java_method` SQL functions (`CallMethodViaReflection`) may invoke.
    
    Note that this is a new feature for operators of multi-tenant Spark server 
service (like `Spark Connect Server` or `Spark Thrift Server`), not a bug fix.
    
    ### Why are the changes needed?
    
    `reflect`/`java_method` can invoke arbitrary static Java methods. When 
Spark runs as a multi-tenant server accepting SQL from untrusted clients (e.g. 
a Spark Connect server or the Thrift JDBC/ODBC server), a client could call 
sensitive methods such as `java.lang.System.exit` to kill the server JVM. A 
static allow list lets admins restrict callable methods, and it cannot be 
relaxed at runtime.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No behavior change because the default value of new static SQL config 
`spark.sql.reflect.allowList` is empty which means allow all. When set, calls 
not matching any pattern fail during analysis:
    
    ```
    $ spark-sql --conf "spark.sql.reflect.allowList=java\.util\.UUID\..*"
    spark-sql> SELECT reflect('java.lang.System', 'exit', 0);
    [DATATYPE_MISMATCH.METHOD_NOT_ALLOWED] ... the method exit of the class
    java.lang.System does not match the allow list "spark.sql.reflect.allowList"
    ```
    
    ### How was this patch tested?
    
    New unit tests in `CallMethodViaReflectionSuite` (empty / matching / 
non-matching / regex, and invalid-pattern rejection). Also ran 
`SparkThrowableSuite`.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 4.8)
    
    Closes #56509 from dongjoon-hyun/SPARK-57448.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 .../src/main/resources/error/error-conditions.json |  5 ++
 .../expressions/CallMethodViaReflection.scala      | 20 ++++++++
 .../apache/spark/sql/internal/StaticSQLConf.scala  | 17 +++++++
 .../expressions/CallMethodViaReflectionSuite.scala | 53 +++++++++++++++++++++-
 4 files changed, 94 insertions(+), 1 deletion(-)

diff --git a/common/utils/src/main/resources/error/error-conditions.json 
b/common/utils/src/main/resources/error/error-conditions.json
index e0bea26dc960..ad6e0dfc4c23 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -1866,6 +1866,11 @@
           "Input to the <functionName> should have been two maps with 
compatible key types, but it's [<leftType>, <rightType>]."
         ]
       },
+      "METHOD_NOT_ALLOWED" : {
+        "message" : [
+          "the method <methodName> of the class <className> does not match the 
allow list <config>."
+        ]
+      },
       "NON_FOLDABLE_INPUT" : {
         "message" : [
           "the input <inputName> should be a foldable <inputType> expression; 
however, got <inputExpr>."
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/CallMethodViaReflection.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/CallMethodViaReflection.scala
index 94d9b305d3b0..90b9f19cf8d0 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/CallMethodViaReflection.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/CallMethodViaReflection.scala
@@ -26,6 +26,7 @@ import 
org.apache.spark.sql.catalyst.analysis.{FunctionRegistry, TypeCheckResult
 import 
org.apache.spark.sql.catalyst.analysis.TypeCheckResult.{DataTypeMismatch, 
TypeCheckSuccess}
 import org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback
 import org.apache.spark.sql.errors.{QueryCompilationErrors, QueryErrorsBase}
+import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
 import org.apache.spark.sql.internal.types.StringTypeWithCollation
 import org.apache.spark.sql.types._
 import org.apache.spark.unsafe.types.UTF8String
@@ -126,6 +127,14 @@ case class CallMethodViaReflection(
 
       unexpectedParameter match {
         case Some(mismatch) => mismatch
+        case _ if !methodAllowed =>
+          DataTypeMismatch(
+            errorSubClass = "METHOD_NOT_ALLOWED",
+            messageParameters = Map(
+              "methodName" -> methodName,
+              "className" -> className,
+              "config" -> toSQLConf(StaticSQLConf.REFLECT_ALLOW_LIST.key))
+          )
         case _ if !classExists =>
           DataTypeMismatch(
             errorSubClass = "UNEXPECTED_CLASS_TYPE",
@@ -176,6 +185,17 @@ case class CallMethodViaReflection(
   /** Name of the method */
   @transient private lazy val methodName = 
children(1).eval(null).asInstanceOf[UTF8String].toString
 
+  /**
+   * True if the canonical `class.method` name fully matches one of the 
regular expressions in
+   * the allow list configured by [[StaticSQLConf.REFLECT_ALLOW_LIST]]. An 
empty list (the default)
+   * allows every call.
+   */
+  @transient private lazy val methodAllowed: Boolean = {
+    val allowList = SQLConf.get.getConf(StaticSQLConf.REFLECT_ALLOW_LIST)
+    val canonicalName = s"$className.$methodName"
+    allowList.isEmpty || allowList.exists(canonicalName.matches)
+  }
+
   /** The reflection method. */
   @transient lazy val method: Method = CallMethodViaReflection
     .findMethod(className, methodName, 
argExprs.map(_.dataType).toImmutableArraySeq).orNull
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/StaticSQLConf.scala 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/StaticSQLConf.scala
index ad9106ee3e06..9a3bc9d69d43 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/StaticSQLConf.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/StaticSQLConf.scala
@@ -20,6 +20,8 @@ package org.apache.spark.sql.internal
 import java.util.Locale
 import java.util.concurrent.TimeUnit
 
+import scala.util.Try
+
 import org.apache.spark.internal.config.ConfigBindingPolicy
 import 
org.apache.spark.sql.connector.catalog.CatalogManager.SESSION_CATALOG_NAME
 import org.apache.spark.util.Utils
@@ -325,4 +327,19 @@ object StaticSQLConf {
     .version("4.0.0")
     .booleanConf
     .createWithDefault(true)
+
+  val REFLECT_ALLOW_LIST = buildStaticConf("spark.sql.reflect.allowList")
+    .doc("A comma-separated allow list of regular expressions matched against 
the canonical " +
+      "static method name (in the form `class.method`, e.g. 
`java.util.UUID.randomUUID`) " +
+      "that the `reflect` and `java_method` SQL functions are allowed to 
invoke. A method is " +
+      "allowed when its `class.method` name fully matches at least one of the 
patterns. " +
+      "When the list is empty (the default), all such invocations are 
allowed.")
+    .version("4.3.0")
+    .withBindingPolicy(ConfigBindingPolicy.NOT_APPLICABLE)
+    .stringConf
+    .toSequence
+    .checkValue(
+      _.forall(pattern => Try(pattern.r).isSuccess),
+      "Every entry must be a valid regular expression.")
+    .createWithDefault(Nil)
 }
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CallMethodViaReflectionSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CallMethodViaReflectionSuite.scala
index 995b519bd05d..bd7aed831ae0 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CallMethodViaReflectionSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CallMethodViaReflectionSuite.scala
@@ -19,12 +19,14 @@ package org.apache.spark.sql.catalyst.expressions
 
 import java.sql.Timestamp
 
-import org.apache.spark.{SPARK_DOC_ROOT, SparkFunSuite}
+import org.apache.spark.{SPARK_DOC_ROOT, SparkFunSuite, 
SparkIllegalArgumentException}
 import org.apache.spark.sql.AnalysisException
 import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch
 import org.apache.spark.sql.catalyst.expressions.Cast.toSQLType
 import 
org.apache.spark.sql.catalyst.expressions.codegen.GenerateUnsafeProjection
+import org.apache.spark.sql.catalyst.util.QuotingUtils.toSQLConf
 import org.apache.spark.sql.catalyst.util.TypeUtils.ordinalNumber
+import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
 import org.apache.spark.sql.types._
 
 /** A static class for testing purpose. */
@@ -51,6 +53,17 @@ class CallMethodViaReflectionSuite extends SparkFunSuite 
with ExpressionEvalHelp
   private val staticClassName = 
ReflectStaticClass.getClass.getName.stripSuffix("$")
   private val dynamicClassName = classOf[ReflectDynamicClass].getName
 
+  /**
+   * Runs `f` with the given regular expression patterns set as the reflect 
allow list. The config
+   * is static, so it cannot be set via `withSQLConf`; we install a fresh 
`SQLConf` instead. Using
+   * `setConfString` keeps the `checkValue` validation in the path.
+   */
+  private def withAllowList(patterns: String*)(f: => Unit): Unit = {
+    val conf = new SQLConf()
+    conf.setConfString(StaticSQLConf.REFLECT_ALLOW_LIST.key, 
patterns.mkString(","))
+    SQLConf.withExistingConf(conf)(f)
+  }
+
   test("findMethod via reflection for static methods") {
     assert(findMethod(staticClassName, "method1", Seq.empty).exists(_.getName 
== "method1"))
     assert(findMethod(staticClassName, "method2", Seq(IntegerType)).isDefined)
@@ -159,6 +172,44 @@ class CallMethodViaReflectionSuite extends SparkFunSuite 
with ExpressionEvalHelp
     checkEvaluation(createExpr(staticClassName, "method4", 4, "four"), 
"m4four")
   }
 
+  test("SPARK-57448: allow list restricts reflective calls when set") {
+    def methodNotAllowed(methodName: String): DataTypeMismatch =
+      DataTypeMismatch(
+        errorSubClass = "METHOD_NOT_ALLOWED",
+        messageParameters = Map(
+          "methodName" -> methodName,
+          "className" -> staticClassName,
+          "config" -> toSQLConf(StaticSQLConf.REFLECT_ALLOW_LIST.key))
+      )
+
+    // An empty allow list (the default) allows every call.
+    assert(createExpr(staticClassName, 
"method1").checkInputDataTypes().isSuccess)
+
+    // When set, only methods whose canonical name matches a pattern are 
allowed.
+    withAllowList(s"$staticClassName.method1") {
+      assert(createExpr(staticClassName, 
"method1").checkInputDataTypes().isSuccess)
+      assert(createExpr(staticClassName, "method2", 2).checkInputDataTypes() ==
+        methodNotAllowed("method2"))
+    }
+
+    // Regular expression patterns are supported.
+    withAllowList("java\\.util\\..*") {
+      assert(createExpr("java.util.UUID", 
"randomUUID").checkInputDataTypes().isSuccess)
+    }
+  }
+
+  test("SPARK-57448: invalid regular expression in the allow list is 
rejected") {
+    checkError(
+      exception = intercept[SparkIllegalArgumentException] {
+        withAllowList("(unclosed") {}
+      },
+      condition = "INVALID_CONF_VALUE.REQUIREMENT",
+      parameters = Map(
+        "confName" -> StaticSQLConf.REFLECT_ALLOW_LIST.key,
+        "confValue" -> "(unclosed",
+        "confRequirement" -> "Every entry must be a valid regular 
expression."))
+  }
+
   test("escaping of class and method names") {
     GenerateUnsafeProjection.generate(
       CallMethodViaReflection(Seq(Literal("\"quote"), Literal("\"quote"), 
Literal(null))) :: Nil)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to