You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kyuubi.apache.org by ch...@apache.org on 2026/07/28 12:59:08 UTC
(kyuubi) branch master updated: [KYUUBI #7585] Queries with 'TRANSFORM ... USING ' must be rejected
This is an automated email from the ASF dual-hosted git repository.
pan3793 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kyuubi.git
The following commit(s) were added to refs/heads/master by this push:
new 25d43f16d4 [KYUUBI #7585] Queries with 'TRANSFORM ... USING <shell-script>' must be rejected
25d43f16d4 is described below
commit 25d43f16d4a9187478e43590623de359b7d87a1d
Author: attilapiros <pi...@gmail.com>
AuthorDate: Tue Jul 28 20:58:58 2026 +0800
[KYUUBI #7585] Queries with 'TRANSFORM ... USING <shell-script>' must be rejected
### Why are the changes needed?
This PR adds a configurable plan-node deny list to kyuubi-spark-authz. The first (and currently only) entry blocks `ScriptTransformation` — the Spark SQL `TRANSFORM ... USING <script>` feature — which would otherwise let any SQL user execute arbitrary shell commands on Spark executors, bypassing all Ranger data-access controls.
### How was this patch tested?
Added a new unit test suite `NodeDenyListCheckerSuite`:
```
Discovery starting.
Discovery completed in 134 milliseconds.
Run starting. Expected test count is: 3
NodeDenyListCheckerSuite:
- plain SELECT is allowed
- TRANSFORM ... USING is rejected
- TRANSFORM ... USING inside a subquery is rejected
Run completed in 4 seconds, 858 milliseconds.
Total number of tests run: 3
Suites: completed 2, aborted 0
Tests: succeeded 3, failed 0, canceled 0, ignored 0, pending 0
```
### Was this patch authored or co-authored using generative AI tooling?
Yes.
Assisted-by: Claude:claude-sonnet
Closes #7586 from attilapiros/master.
Closes #7585
1929982a2 [attilapiros] [KYUUBI #7585] Queries with 'TRANSFORM ... USING <shell-script>' must be rejected
Authored-by: attilapiros <pi...@gmail.com>
Signed-off-by: Cheng Pan <ch...@apache.org>
---
.../src/main/resources/denied_plan_node_spec.json | 4 ++
.../spark/authz/ranger/RangerSparkExtension.scala | 3 +-
.../authz/rule/config/NodeDenyListChecker.scala | 39 ++++++++++++
.../plugin/spark/authz/serde/CommandSpec.scala | 8 +++
.../kyuubi/plugin/spark/authz/serde/package.scala | 6 ++
.../authz/rule/NodeDenyListCheckerSuite.scala | 71 ++++++++++++++++++++++
6 files changed, 130 insertions(+), 1 deletion(-)
diff --git a/extensions/spark/kyuubi-spark-authz/src/main/resources/denied_plan_node_spec.json b/extensions/spark/kyuubi-spark-authz/src/main/resources/denied_plan_node_spec.json
new file mode 100644
index 0000000000..54b83aacdc
--- /dev/null
+++ b/extensions/spark/kyuubi-spark-authz/src/main/resources/denied_plan_node_spec.json
@@ -0,0 +1,4 @@
+[ {
+ "classname" : "org.apache.spark.sql.catalyst.plans.logical.ScriptTransformation",
+ "message" : "TRANSFORM ... USING scripts are not allowed"
+} ]
diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtension.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtension.scala
index 93f9a37dfc..bbe1b9c2c6 100644
--- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtension.scala
+++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/ranger/RangerSparkExtension.scala
@@ -21,7 +21,7 @@ import org.apache.spark.sql.SparkSessionExtensions
import org.apache.kyuubi.plugin.spark.authz.rule.{RuleEliminateMarker, RuleEliminatePermanentViewMarker, RuleEliminateTypeOf}
import org.apache.kyuubi.plugin.spark.authz.rule.Authorization.RuleClearAuthzTag
-import org.apache.kyuubi.plugin.spark.authz.rule.config.AuthzConfigurationChecker
+import org.apache.kyuubi.plugin.spark.authz.rule.config.{AuthzConfigurationChecker, NodeDenyListChecker}
import org.apache.kyuubi.plugin.spark.authz.rule.datamasking.{RuleApplyDataMaskingStage0, RuleApplyDataMaskingStage1}
import org.apache.kyuubi.plugin.spark.authz.rule.expression.RuleApplyTypeOfMarker
import org.apache.kyuubi.plugin.spark.authz.rule.permanentview.RuleApplyPermanentViewMarker
@@ -54,6 +54,7 @@ class RangerSparkExtension extends (SparkSessionExtensions => Unit) {
// before the optimizer runs. The tag is only set by optimizer rules, so a single
// post-hoc pass is sufficient - no need for fixed-point iteration.
v1.injectPostHocResolutionRule(_ => RuleClearAuthzTag)
+ v1.injectCheckRule(NodeDenyListChecker)
v1.injectResolutionRule(_ => RuleReplaceShowObjectCommands)
v1.injectResolutionRule(_ => RuleApplyPermanentViewMarker)
v1.injectResolutionRule(_ => RuleApplyTypeOfMarker)
diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/config/NodeDenyListChecker.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/config/NodeDenyListChecker.scala
new file mode 100644
index 0000000000..9c7d90bdff
--- /dev/null
+++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/rule/config/NodeDenyListChecker.scala
@@ -0,0 +1,39 @@
+/*
+ * 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.kyuubi.plugin.spark.authz.rule.config
+
+import org.apache.spark.sql.SparkSession
+import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan
+
+import org.apache.kyuubi.plugin.spark.authz.AccessControlException
+import org.apache.kyuubi.plugin.spark.authz.serde.DENIED_PLAN_NODE_SPECS
+
+/**
+ * Rejects queries whose plan contains any node listed in denied_plan_node_spec.json.
+ * Matching is by exact classname; add entries to the spec file to deny additional node types
+ * without changing code.
+ */
+case class NodeDenyListChecker(spark: SparkSession) extends (LogicalPlan => Unit) {
+ override def apply(plan: LogicalPlan): Unit = {
+ plan.foreach { node =>
+ DENIED_PLAN_NODE_SPECS.get(node.getClass.getName).foreach { spec =>
+ throw new AccessControlException(spec.message)
+ }
+ }
+ }
+}
diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/CommandSpec.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/CommandSpec.scala
index c4fd721ca9..2e6e1b6c1f 100644
--- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/CommandSpec.scala
+++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/CommandSpec.scala
@@ -99,6 +99,14 @@ case class TableCommandSpec(
}
}
+/**
+ * A specification for a plan node that should be unconditionally denied.
+ *
+ * @param classname the fully-qualified plan node classname
+ * @param message the error message surfaced in the AccessControlException
+ */
+case class DeniedPlanNodeSpec(classname: String, message: String)
+
case class ScanSpec(
classname: String,
scanDescs: Seq[ScanDesc],
diff --git a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/package.scala b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/package.scala
index 5cc1c8c19a..f9a8c38ff5 100644
--- a/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/package.scala
+++ b/extensions/spark/kyuubi-spark-authz/src/main/scala/org/apache/kyuubi/plugin/spark/authz/serde/package.scala
@@ -44,6 +44,12 @@ package object serde {
def loadExtractorsToMap[T <: Extractor](implicit ct: ClassTag[T]): Map[String, T] =
loadFromServiceLoader[T]()(ct).map { e: T => (e.key, e) }.toMap
+ final lazy val DENIED_PLAN_NODE_SPECS: Map[String, DeniedPlanNodeSpec] = {
+ val is = getClass.getClassLoader.getResourceAsStream("denied_plan_node_spec.json")
+ mapper.readValue(is, new TypeReference[Array[DeniedPlanNodeSpec]] {})
+ .map(e => (e.classname, e)).toMap
+ }
+
final lazy val DB_COMMAND_SPECS: Map[String, DatabaseCommandSpec] = {
val is = getClass.getClassLoader.getResourceAsStream("database_command_spec.json")
mapper.readValue(is, new TypeReference[Array[DatabaseCommandSpec]] {})
diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/rule/NodeDenyListCheckerSuite.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/rule/NodeDenyListCheckerSuite.scala
new file mode 100644
index 0000000000..bc075e3a46
--- /dev/null
+++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/rule/NodeDenyListCheckerSuite.scala
@@ -0,0 +1,71 @@
+/*
+ * 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.kyuubi.plugin.spark.authz.rule
+
+import org.scalatest.BeforeAndAfterAll
+
+import org.apache.kyuubi.KyuubiFunSuite
+import org.apache.kyuubi.plugin.spark.authz.{AccessControlException, SparkSessionProvider}
+import org.apache.kyuubi.plugin.spark.authz.rule.config.NodeDenyListChecker
+
+class NodeDenyListCheckerSuite extends KyuubiFunSuite with SparkSessionProvider
+ with BeforeAndAfterAll {
+
+ // TRANSFORM ... USING requires Hive catalog support
+ override protected val catalogImpl: String = "hive"
+
+ override def afterAll(): Unit = {
+ spark.stop()
+ super.afterAll()
+ }
+
+ test("plain SELECT is allowed") {
+ sql("CREATE TABLE IF NOT EXISTS ndl_plain (k INT, v STRING) USING hive")
+ try {
+ val plan = sql("SELECT k, v FROM ndl_plain").queryExecution.analyzed
+ NodeDenyListChecker(spark).apply(plan)
+ } finally {
+ sql("DROP TABLE IF EXISTS ndl_plain PURGE")
+ }
+ }
+
+ test("TRANSFORM ... USING is rejected") {
+ sql("CREATE TABLE IF NOT EXISTS ndl_transform (k INT, v STRING) USING hive")
+ try {
+ val plan =
+ sql("SELECT TRANSFORM(k, v) USING 'cat' AS (k STRING, v STRING) FROM ndl_transform")
+ .queryExecution.analyzed
+ intercept[AccessControlException](NodeDenyListChecker(spark).apply(plan))
+ } finally {
+ sql("DROP TABLE IF EXISTS ndl_transform PURGE")
+ }
+ }
+
+ test("TRANSFORM ... USING inside a subquery is rejected") {
+ sql("CREATE TABLE IF NOT EXISTS ndl_sub (k INT, v STRING) USING hive")
+ try {
+ val plan = sql(
+ """SELECT * FROM (
+ | SELECT TRANSFORM(k, v) USING 'cat' AS (k STRING, v STRING) FROM ndl_sub
+ |) t""".stripMargin).queryExecution.analyzed
+ intercept[AccessControlException](NodeDenyListChecker(spark).apply(plan))
+ } finally {
+ sql("DROP TABLE IF EXISTS ndl_sub PURGE")
+ }
+ }
+}