Apache Spark 4.2.0 - 高危 - 远程 - 文件写入 - Connect archives hash-fragment dest
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-connect-archive-fragment
类别: 文件写入 可达性: 远程
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
Spark Connect AddArtifacts 中形如 archives/<file>#<fragment> 的名称会按井号 / 数字符号字符进行拆分。zip 字节流会经过 concatenatePaths。而 fragment 不会。随后 SparkContext.addFile(..., isArchive=true) 会在 driver 上执行 new File(root, uri.getFragment)、deleteRecursively(dest) 以及 unpack。Executor 会构建相同的 dest。SPARK-33530 将 #name 记录为 SparkFiles 下的目录名。fragment 为 ../<outside> 时即为 dest。
Connect archives/<file>#../<path> 会以 Spark UID 身份在会话 SparkFiles 目录之外解包并执行 deleteRecursively。
| ID | 暂无 CVE |
| 类别 | 文件写入 |
| 可达性 | 远程(Spark Connect 客户端;AddArtifacts 且 archive=True) |
| CWE | CWE-22 |
| CVSS | 高危:8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H |
| 产品 | Apache Spark |
| 受影响版本 | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) Connect archives/ 井号 fragment dest |
| 认证 | Connect 客户端。默认 Connect 无 PSK(spark.connect.authenticate.token 未设置)。若启用 PSK / 网络 ACL 则为 PR:L;默认开放 Connect 为 PR:N。 |
| 许可证 | GNU Affero GPL v3.0 |
| 实验环境 | 仅 127.0.0.1 |
我固定了标签 v4.2.0 的 peel 32f72996(2026-07-11,v4.2.0-rc6)。实验环境是一个 Java gadget,复现了固定版本中 StagedArtifact 的井号拆分、ArtifactUtils.concatenatePaths、new File(root, fragment)、deleteRecursively 以及 dest 内 zip 解包。回环 HTTP 位于 127.0.0.1:18930。官方 PySpark 已经会将 URI fragment 复制到归档名称中。
这不是 RCE。走出 dest 的 zip 成员会被拒绝(Hadoop FileUtil.unZip / HADOOP-13649 类)。遗留问题在于 dest 本身。
| 方法 | 汇聚点 | 权限 |
|---|---|---|
Connect AddArtifacts 名称 archives/<file>#../<outside> | SparkContext.addFile dest new File(root, uri.getFragment) 然后 Utils.unpack(SparkContext.scala:1891-1900) | 完整性(实验证明): 以 Spark UID 身份在会话 SparkFiles 之外写入归档成员。实验 dest 规范路径 /lab/oracle/SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS,uid 1000,dest-outside=yes。 |
| 同一 dest,在解包之前 | Utils.deleteRecursively(dest)(SparkContext.scala:1899) | 可用性(实验证明): dest 金丝雀文件消失。实验 deleteRecursively=yes;dest 下的 canary.txt 未能存活。同级的 /lab/oracle/canary-keep.txt 保留。 |
| 在获取归档的每个 executor 上使用同一原语 | Executor dest new File(root, sourceURI.getFragment) 然后 deleteRecursively + unpack(Executor.scala:1468-1477) | 完整性 / 可用性(推断): executor 上构建相同的 dest。实验为 driver 侧 gadget。 |
制品名称 files/../../... 或 archives/../../... | ArtifactUtils.concatenatePaths(ArtifactUtils.scala:26-39) | 已关闭(实验证明): INVALID_ARTIFACT_PATH。 |
Unix 绝对 fragment /abs | new File(root, "/abs") | 在 Unix 上已关闭(实验证明): 父路径未被忽略;dest 拼接在 root 之下。实验 dest /lab/root/lab/oracle/abs-...,outside=false,abs 金丝雀保留。 |
本会话 jars/ / classes/ 添加 | 会话 URLClassLoader | 作为遗留问题已关闭: 本会话 JAR 加载是设计使然。不属于本发现。 |
| 跨会话 jar 加载 | Connect 隔离 spark.sql.artifact.isolation.enabled=true | 已关闭: 隔离会话读取 addedJars(sessionUUID)。 |
实验证明 vs 推断: 井号 fragment dest 位于 SparkFiles 之外、dest 金丝雀被删除、同级金丝雀保留、files/ 与 archives/ 名称拼接被拒绝、Unix 绝对 fragment 拼接在 root 之下、uid 1000、dest 内 zip 解包:实验证明。executor 获取同一 dest:推断(相同构造函数)。覆盖 driver classpath 上后续加载的类:推断链,未演示,未夸大为 RCE。
前提条件: 一个能够以 archive=True 执行 AddArtifacts 的 Connect 客户端。官方 PySpark spark.addArtifact(..., archive=True) 已经会将非空 URI fragment 放入名称中(python/pyspark/sql/connect/client/artifact.py:223-236)。默认 Connect 无 PSK。
| 攻击面 | 实验 |
|---|---|
制品名称 .. / 绝对路径经由 concatenatePaths | files-concat=rejected INVALID_ARTIFACT_PATH |
archives/ 名称遍历 archives/../../... | 同样 INVALID_ARTIFACT_PATH |
Unix new File(root, "/abs") | dest 位于 root 之下;abs oracle 金丝雀保留 |
| 受限 dest 下的 zip 成员 zip-slip | gadget 拒绝逃逸 dest 的成员(HADOOP-13649 类)。不属于本遗留问题。 |
files/ 路径拼接(无井号拆分) | 不取 fragment;名称仍走 concatenatePaths |
| 跨会话 jar 隔离 | Connect 默认开启 |
| 开放 Connect 上的未认证 SQL / Python | 设计使然的作业提交(SECURITY.md FAQ)。不属于本遗留问题。 |
| 作业提交 / ML 模型加载 | 设计使然 |
StagedArtifact 仅拆分以 archives 加服务器 File.separator 开头的名称,且仅按 # 拆分:
val splits = name.split("#")
if (splits.length == 2) {
(splits(0), Some(splits(1)))
}
splits(0) 通过 concatenatePaths 暂存。splits(1) 保留为 fragment。ArtifactManager 将该 fragment 附加到会话文件服务器 URI 并调用 SparkContext.addArchive。dest 为 new File(root, uri.getFragment)。SPARK-33530 测试了 sc.addArchive(s"${jarFile.getAbsolutePath}#foo") 作为 SparkFiles 下的目录名。
首轮排查曾声称 Unix 绝对 fragment 会忽略 root(java.io.File 对绝对子路径忽略父路径)。在 Unix 上,new File(root, "/abs") 会去掉前导斜杠并拼接在 root 之下。父路径仅在 Windows 上被忽略。此固定版本上的高危遍历是 #../。
cd lab
./run.sh
回环 127.0.0.1:18930。Compose 项目 spark-connect-archive-fragment。镜像 eclipse-temurin:21-jdk-jammy,运行时 uid 1000。见证文件 SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS。最后一行:
SUCCESS SPARK-CONNECT-ARCHIVE-FRAGMENT how=gadget dest-outside=yes deleteRecursively=yes files-concat=rejected uid=1000 SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS
CONTROL:archives/payload.zip 解包到 /lab/root 之下。ATTACK:archives/payload-frag.zip#../oracle/SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS dest 规范路径 /lab/oracle/...,金丝雀被删除,pwned.txt 以 uid 1000 写入。Unix 绝对 fragment 拼接在 root 之下。栈在 EXIT 时拆除。
将归档 fragment 通过与制品名称相同的 stay-under-root 检查(concatenatePaths,或拒绝 .. / 绝对路径)。保持 SPARK-33530 的 #name 作为 SparkFiles 下的单一目录组件。不要对未通过该检查的 dest 执行 deleteRecursively。Executor 使用相同的构造函数;两者都要修复。
在此之前,不要将 Spark Connect 暴露给任何你尚未信任其拥有 Spark UID 文件系统访问权限的人。在 spark.connect.authenticate.token 上设置 PSK 可将门槛提高到令牌持有者。它不会净化 fragment。