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
Имена AddArtifacts в Spark Connect вида 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> распаковывает и выполняет deleteRecursively вне каталога SparkFiles сессии от имени Spark UID.
| ID | CVE пока нет |
| Класс | Запись файлов |
| Достижимость | Удалённая (клиент Spark Connect; AddArtifacts с archive=True) |
| 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 не задан). PR:L при PSK / сетевом ACL; PR:N при открытом Connect по умолчанию. |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1 |
Я зафиксировал тег v4.2.0 peel 32f72996 (2026-07-11, v4.2.0-rc6). Лаборатория — это Java-гаджет зафиксированного разбиения хеша StagedArtifact, ArtifactUtils.concatenatePaths, new File(root, fragment), deleteRecursively и распаковки zip с ограничением dest. Loopback HTTP на 127.0.0.1:18930. Официальный PySpark уже копирует фрагмент URI в имя архива.
Это не RCE. Элементы zip, выходящие за пределы dest, отклоняются (класс Hadoop FileUtil.unZip / HADOOP-13649). Остаётся сам dest.
| Метод | Sink | Привилегия |
|---|---|---|
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) | Доступность (доказано в лаборатории): canary в dest исчез. Лаборатория deleteRecursively=yes; canary.txt под dest не сохранился. Соседний /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 canary сохранён. |
Добавление jars/ / classes/ в собственной сессии | URLClassLoader сессии | Закрыто как остаточное: загрузка JAR в собственной сессии — по замыслу. Не эта находка. |
| Загрузка jar между сессиями | Изоляция Connect spark.sql.artifact.isolation.enabled=true | Закрыто: изолированные сессии читают addedJars(sessionUUID). |
Доказано в лаборатории vs предположительно: dest из хеш-фрагмента вне SparkFiles, canary в dest удалён, соседний canary сохранён, конкатенация имён files/ и archives/ отклонена, Unix-абсолютный фрагмент присоединяется под root, uid 1000, распаковка zip с ограничением dest: лаборатория. Забор того же dest исполнителем: предположительно (тот же конструктор). Перезапись позже загруженного класса на classpath драйвера: предполагаемая цепочка, не продемонстрирована, не раздута до RCE.
Требуется: клиент Connect, способный выполнить AddArtifacts с archive=True. Официальный 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 oracle canary сохранён |
| Zip-slip элемента zip под ограниченным dest | гаджет отклоняет элементы, выходящие за dest (класс HADOOP-13649). Не это остаточное. |
Конкатенация пути files/ (без разбиения по хешу) | фрагмент не берётся; имя всё ещё concatenatePaths |
| Изоляция jar между сессиями | по умолчанию включена для Connect |
| Неаутентифицированный SQL / Python на открытом Connect | отправка заданий по замыслу (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
Loopback 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