Apache Spark 4.2.0 - Alta - Remoto - Gravação de arquivo - Connect archives hash-fragment dest
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-connect-archive-fragment
Classe: Escrita de arquivo Alcance: Remoto
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
Nomes de AddArtifacts do Spark Connect no formato archives/<file>#<fragment> são divididos no caractere hash / cerquilha. Os bytes do zip passam por concatenatePaths. O fragmento não. SparkContext.addFile(..., isArchive=true) então faz new File(root, uri.getFragment), deleteRecursively(dest) e unpack no driver. Os executores constroem o mesmo dest. SPARK-33530 documentou #name como um nome de diretório sob SparkFiles. Um fragmento de ../<outside> é o dest.
Connect archives/<file>#../<path> descompacta e faz deleteRecursively fora do diretório SparkFiles da sessão como o UID do Spark.
| ID | sem CVE ainda |
| Classe | Escrita de arquivo |
| Alcance | Remoto (cliente Spark Connect; AddArtifacts com archive=True) |
| CWE | CWE-22 |
| CVSS | Alto: 8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H |
| Produto | Apache Spark |
| Afetado | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) dest de fragmento hash de archives/ do Connect |
| Autenticação | Cliente Connect. O Connect padrão não tem PSK (spark.connect.authenticate.token não definido). PR:L se PSK / ACL de rede; PR:N no Connect aberto padrão. |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1 |
Fiz pin da tag v4.2.0 peel 32f72996 (2026-07-11, v4.2.0-rc6). O lab é um gadget Java da divisão de hash do StagedArtifact fixado, ArtifactUtils.concatenatePaths, new File(root, fragment), deleteRecursively e descompactação de zip contida no dest. HTTP em loopback em 127.0.0.1:18930. O PySpark oficial já copia um fragmento de URI para o nome do arquivo.
Isto não é RCE. Membros de zip que saem do dest são rejeitados (classe Hadoop FileUtil.unZip / HADOOP-13649). O que sobra é o próprio dest.
| Método | Sink | Privilégio |
|---|---|---|
Connect AddArtifacts nome archives/<file>#../<outside> | dest de SparkContext.addFile new File(root, uri.getFragment) então Utils.unpack (SparkContext.scala:1891-1900) | Integridade (comprovado em lab): escrever membros do arquivo como o UID do Spark fora do SparkFiles da sessão. Dest do lab canônico /lab/oracle/SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS, uid 1000, dest-outside=yes. |
| Mesmo dest, antes do unpack | Utils.deleteRecursively(dest) (SparkContext.scala:1899) | Disponibilidade (comprovado em lab): canário do dest sumiu. Lab deleteRecursively=yes; canary.txt sob o dest não sobreviveu. Irmão /lab/oracle/canary-keep.txt mantido. |
| Mesmo primitivo em cada executor que busca o arquivo | dest do Executor new File(root, sourceURI.getFragment) então deleteRecursively + unpack (Executor.scala:1468-1477) | Integridade / disponibilidade (inferido): mesma construção de dest nos executores. O lab é o gadget do lado do driver. |
Nome do artefato files/../../... ou archives/../../... | ArtifactUtils.concatenatePaths (ArtifactUtils.scala:26-39) | Fechado (comprovado em lab): INVALID_ARTIFACT_PATH. |
Fragmento absoluto Unix /abs | new File(root, "/abs") | Fechado no Unix (comprovado em lab): o pai não é ignorado; o dest se junta sob o root. Dest do lab /lab/root/lab/oracle/abs-..., outside=false, canário abs mantido. |
Adição de jars/ / classes/ na própria sessão | URLClassLoader da sessão | Fechado como sobra: carregamento de JAR na própria sessão é por design. Não é este achado. |
| Carregamento de jar entre sessões | Isolamento do Connect spark.sql.artifact.isolation.enabled=true | Fechado: sessões isoladas leem addedJars(sessionUUID). |
Comprovado em lab vs inferido: dest de fragmento hash fora do SparkFiles, canário do dest deletado, canário irmão mantido, concat de nome files/ e archives/ rejeitada, fragmento absoluto Unix se junta sob o root, uid 1000, descompactação de zip contida no dest: lab. Busca do mesmo dest pelo executor: inferido (mesmo construtor). Sobrescrita de uma classe carregada posteriormente no classpath do driver: cadeia inferida, não demonstrada, não inflada para RCE.
Requer: um cliente Connect que possa AddArtifacts com archive=True. O PySpark oficial spark.addArtifact(..., archive=True) já coloca um fragmento de URI não vazio no nome (python/pyspark/sql/connect/client/artifact.py:223-236). O Connect padrão não tem PSK.
| Superfície | Lab |
|---|---|
Nome de artefato .. / absoluto através de concatenatePaths | files-concat=rejected INVALID_ARTIFACT_PATH |
Caminhada no nome archives/ archives/../../... | mesmo INVALID_ARTIFACT_PATH |
Unix new File(root, "/abs") | dest sob o root; canário do oráculo abs mantido |
| Zip-slip de membro de zip sob um dest contido | gadget rejeita membros que escapam do dest (classe HADOOP-13649). Não é esta sobra. |
Concat de caminho files/ (sem divisão de hash) | fragmento não é tomado; nome ainda concatenatePaths |
| Isolamento de jar entre sessões | padrão ativado para Connect |
| SQL / Python sem autenticação no Connect aberto | envio de job por design (SECURITY.md FAQ). Não é esta sobra. |
| Envio de job / carregamento de modelo ML | por design |
StagedArtifact divide apenas nomes que começam com archives mais o File.separator do servidor, e apenas em #:
val splits = name.split("#")
if (splits.length == 2) {
(splits(0), Some(splits(1)))
}
splits(0) é preparado via concatenatePaths. splits(1) é mantido como fragment. ArtifactManager anexa esse fragmento à URI do file-server da sessão e chama SparkContext.addArchive. O dest é new File(root, uri.getFragment). Os testes do SPARK-33530 usam sc.addArchive(s"${jarFile.getAbsolutePath}#foo") como um nome de diretório sob SparkFiles.
A primeira passada da caça alegou que um fragmento absoluto Unix ignora root (ignorar pai do java.io.File para filhos absolutos). No Unix, new File(root, "/abs") remove a barra inicial e se junta sob root. O pai é ignorado apenas no Windows. A caminhada High neste pin é #../.
cd lab
./run.sh
Loopback 127.0.0.1:18930. Projeto Compose spark-connect-archive-fragment. Imagem eclipse-temurin:21-jdk-jammy, uid de runtime 1000. Witness SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS. Última linha:
SUCCESS SPARK-CONNECT-ARCHIVE-FRAGMENT how=gadget dest-outside=yes deleteRecursively=yes files-concat=rejected uid=1000 SPARK-CONNECT-ARCHIVE-FRAGMENT-WITNESS