
Apache Spark 4.2.0 - 高 - リモート - ファイル書き込み - Connect アーカイブのハッシュフラグメント先
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 を通過します。フラグメントは通過しません。その後 SparkContext.addFile(..., isArchive=true) がドライバ上で new File(root, uri.getFragment)、deleteRecursively(dest)、unpack を実行します。エグゼキュータも同じ dest を構築します。SPARK-33530 は #name を SparkFiles 配下のディレクトリ名として文書化しました。../<outside> というフラグメントが dest になります。
Connect の archives/<file>#../<path> は、Spark UID としてセッションの SparkFiles ディレクトリ外で展開し deleteRecursively します。
| ID | CVE 未割当 |
| クラス | ファイル書き込み |
| 到達範囲 | リモート (Spark Connect クライアント; archive=True を指定した AddArtifacts) |
| CWE | CWE-22 |
| CVSS | High: 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/ ハッシュフラグメント 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 のピール 32f72996 (2026-07-11, v4.2.0-rc6) を固定しました。ラボは、固定した StagedArtifact のハッシュ分割、ArtifactUtils.concatenatePaths、new File(root, fragment)、deleteRecursively、および dest 内に収まる zip 展開の Java ガジェットです。127.0.0.1:18930 でのループバック HTTP。公式 PySpark はすでに URI フラグメントをアーカイブ名にコピーしています。
これは 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) | 完全性 (ラボで実証): セッションの SparkFiles 外に Spark UID としてアーカイブメンバーを書き込みます。ラボの 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 の dest new File(root, sourceURI.getFragment) の後 deleteRecursively + unpack (Executor.scala:1468-1477) | 完全性 / 可用性 (推測): エグゼキュータ上でも同じ dest 構築。ラボはドライバ側のガジェットです。 |
アーティファクト 名 files/../../... または archives/../../... | ArtifactUtils.concatenatePaths (ArtifactUtils.scala:26-39) | クローズ (ラボで実証): INVALID_ARTIFACT_PATH。 |
Unix 絶対フラグメント /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 推測: SparkFiles 外へのハッシュフラグメント dest、dest カナリアの削除、兄弟カナリアの保持、files/ と archives/ の名前連結の拒否、Unix 絶対フラグメントが root 配下に結合、uid 1000、dest 内に収まる zip 展開: ラボ。同じ dest のエグゼキュータ取得: 推測 (同じコンストラクタ)。ドライバのクラスパス上で後からロードされるクラスの上書き: 推測される連鎖であり、実証されておらず、RCE に誇張されていません。
必要条件: archive=True で AddArtifacts を実行できる Connect クライアント。公式 PySpark の spark.addArtifact(..., archive=True) はすでに空でない URI フラグメントを名前に含めています (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 オラクルカナリアは保持 |
| 収まる dest 配下での zip メンバーの zip-slip | ガジェットは dest から脱出するメンバーを拒否 (HADOOP-13649 クラス)。本件の残存物ではありません。 |
files/ パス連結 (ハッシュ分割なし) | フラグメントは取得されず、名前は依然 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 はそのフラグメントをセッションのファイルサーバ URI に付加し、SparkContext.addArchive を呼び出します。dest は new File(root, uri.getFragment) です。SPARK-33530 は sc.addArchive(s"${jarFile.getAbsolutePath}#foo") を SparkFiles 配下のディレクトリ名としてテストしています。
ハントの最初のパスでは、Unix 絶対フラグメントが root を無視すると主張しました (java.io.File は絶対パスの子で親を無視)。Unix では new File(root, "/abs") は先頭のスラッシュを除去し root 配下に結合します。親が無視されるのは Windows のみです。このピンでの High の走査は #../ です。
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 絶対フラグメントは root 配下に結合します。スタックは EXIT で破棄されます。
アーカイブフラグメントを、アーティファクト名と同じ root 配下に留まるチェック (concatenatePaths、または .. / 絶対パスの拒否) に通してください。SPARK-33530 の #name は SparkFiles 配下の単一ディレクトリコンポーネントとして維持してください。そのチェックに失敗した dest を deleteRecursively しないでください。エグゼキュータも同じコンストラクタを使用するため、両方を修正してください。
それまでは、Spark UID のファイルシステムをすでに信頼している相手以外に Spark Connect を公開しないでください。spark.connect.authenticate.token の PSK はトークン保持者までハードルを上げるだけです。フラグメントをサニタイズするものではありません。