Apache Spark 4.2.0 - Élevé - À distance - Écriture de fichier - Connect archives hash-fragment dest
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-connect-archive-fragment
Classe : Écriture de fichier Portée : Distante
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
Les noms AddArtifacts de Spark Connect de la forme archives/<file>#<fragment> sont découpés sur le caractère dièse / signe numéro. Les octets du zip passent par concatenatePaths. Le fragment, non. SparkContext.addFile(..., isArchive=true) fait ensuite new File(root, uri.getFragment), deleteRecursively(dest), et unpack sur le driver. Les exécuteurs construisent la même dest. SPARK-33530 a documenté #name comme nom de répertoire sous SparkFiles. Un fragment ../<outside> constitue la dest.
Connect archives/<file>#../<path> décompresse et deleteRecursively en dehors du répertoire SparkFiles de la session en tant qu'UID Spark.
| ID | pas encore de CVE |
| Classe | Écriture de fichier |
| Portée | Distante (client Spark Connect ; AddArtifacts avec archive=True) |
| CWE | CWE-22 |
| CVSS | Élevé : 8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H |
| Produit | Apache Spark |
| Affecté | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) dest du fragment-dièse de Connect archives/ |
| Auth | Client Connect. Connect par défaut n'a pas de PSK (spark.connect.authenticate.token non défini). PR:L si PSK / ACL réseau ; PR:N sur Connect ouvert par défaut. |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 uniquement |
J'ai épinglé le tag v4.2.0 peel 32f72996 (2026-07-11, v4.2.0-rc6). Le lab est un gadget Java du découpage de hash StagedArtifact épinglé, ArtifactUtils.concatenatePaths, new File(root, fragment), deleteRecursively, et décompression zip contenue dans la dest. HTTP en boucle locale sur 127.0.0.1:18930. Le PySpark officiel copie déjà un fragment d'URI dans le nom de l'archive.
Ce n'est pas une RCE. Les membres de zip qui sortent de la dest sont rejetés (classe Hadoop FileUtil.unZip / HADOOP-13649). Le reliquat est la dest elle-même.
| Méthode | Sink | Privilège |
|---|---|---|
Connect AddArtifacts nom archives/<file>#../<outside> | dest SparkContext.addFile new File(root, uri.getFragment) puis Utils.unpack (SparkContext.scala:1891-1900) | Intégrité (prouvé en lab) : écrire des membres d'archive en tant qu'UID Spark en dehors des SparkFiles de la session. Dest du lab canonique /lab/oracle/SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS, uid 1000, dest-outside=yes. |
| Même dest, avant décompression | Utils.deleteRecursively(dest) (SparkContext.scala:1899) | Disponibilité (prouvé en lab) : canari de dest disparu. Lab deleteRecursively=yes ; canary.txt sous la dest n'a pas survécu. Le voisin /lab/oracle/canary-keep.txt conservé. |
| Même primitive sur chaque exécuteur qui récupère l'archive | dest Executor new File(root, sourceURI.getFragment) puis deleteRecursively + unpack (Executor.scala:1468-1477) | Intégrité / disponibilité (inféré) : même construction de dest sur les exécuteurs. Le lab est le gadget côté driver. |
Nom d'artefact files/../../... ou archives/../../... | ArtifactUtils.concatenatePaths (ArtifactUtils.scala:26-39) | Fermé (prouvé en lab) : INVALID_ARTIFACT_PATH. |
Fragment absolu Unix /abs | new File(root, "/abs") | Fermé sur Unix (prouvé en lab) : le parent n'est pas ignoré ; la dest se joint sous root. Dest du lab /lab/root/lab/oracle/abs-..., outside=false, canari abs conservé. |
Ajout jars/ / classes/ de sa propre session | URLClassLoader de session | Fermé en tant que reliquat : le chargement de JAR de sa propre session est by design. Pas cette découverte. |
| Chargement de jar inter-session | Isolation Connect spark.sql.artifact.isolation.enabled=true | Fermé : les sessions isolées lisent addedJars(sessionUUID). |
Prouvé en lab vs inféré : dest du fragment-dièse en dehors des SparkFiles, canari de dest supprimé, canari voisin conservé, concaténation de nom files/ et archives/ rejetée, fragment absolu Unix se joint sous root, uid 1000, décompression zip contenue dans la dest : lab. Récupération par l'exécuteur de la même dest : inféré (même constructeur). Écrasement d'une classe chargée ultérieurement sur le classpath du driver : chaîne inférée, non démontrée, non gonflée en RCE.
Nécessite : un client Connect capable de AddArtifacts avec archive=True. Le PySpark officiel spark.addArtifact(..., archive=True) place déjà un fragment d'URI non vide dans le nom (python/pyspark/sql/connect/client/artifact.py:223-236). Connect par défaut n'a pas de PSK.
| Surface | Lab |
|---|---|
Nom d'artefact .. / absolu via concatenatePaths | files-concat=rejected INVALID_ARTIFACT_PATH |
Nom archives/ walk archives/../../... | même INVALID_ARTIFACT_PATH |
Unix new File(root, "/abs") | dest sous root ; canari oracle abs conservé |
| Zip-slip de membre de zip sous une dest contenue | le gadget rejette les membres qui s'échappent de la dest (classe HADOOP-13649). Pas ce reliquat. |
Concaténation de chemin files/ (pas de découpage sur dièse) | le fragment n'est pas pris ; le nom reste concatenatePaths |
| Isolation de jar inter-session | activée par défaut pour Connect |
| SQL / Python non authentifié sur Connect ouvert | soumission de job by design (FAQ SECURITY.md). Pas ce reliquat. |
| Soumission de job / chargement de modèle ML | by design |
StagedArtifact ne découpe que les noms qui commencent par archives plus le File.separator du serveur, et uniquement sur # :
val splits = name.split("#")
if (splits.length == 2) {
(splits(0), Some(splits(1)))
}
splits(0) est stagé via concatenatePaths. splits(1) est conservé comme fragment. ArtifactManager attache ce fragment à l'URI du file-server de la session et appelle SparkContext.addArchive. La dest est new File(root, uri.getFragment). Les tests SPARK-33530 utilisent sc.addArchive(s"${jarFile.getAbsolutePath}#foo") comme nom de répertoire sous SparkFiles.
La première passe de chasse a prétendu qu'un fragment absolu Unix ignore root (parent-ignore de java.io.File pour les enfants absolus). Sur Unix, new File(root, "/abs") supprime le slash initial et se joint sous root. Le parent n'est ignoré que sur Windows. Le walk Élevé sur ce pin est #../.
cd lab
./run.sh
Boucle locale 127.0.0.1:18930. Projet Compose spark-connect-archive-fragment. Image eclipse-temurin:21-jdk-jammy, uid d'exécution 1000. Témoin SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS. Dernière ligne :
SUCCESS SPARK-CONNECT-ARCHIVE-FRAGMENT how=gadget dest-outside=yes deleteRecursively=yes files-concat=rejected uid=1000 SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS