From 36bb6769778c1716f9f9261f9ab5c72ca4afd2be Mon Sep 17 00:00:00 2001 From: Clemens Portele Date: Thu, 2 Jul 2026 18:17:57 +0200 Subject: [PATCH] fix zip-slip in blob archive extraction BlobExtractorZip resolved each zip entry name against the target directory and wrote it without checking containment, so an entry name with `..` segments could write outside the target directory. Normalize the resolved path and skip any entry that would land outside the target root. Adds BlobExtractorZipSpec. --- .../blobs/infra/BlobExtractorZip.java | 11 ++++- .../blobs/infra/BlobExtractorZipSpec.groovy | 41 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 xtraplatform-blobs/src/test/groovy/de/ii/xtraplatform/blobs/infra/BlobExtractorZipSpec.groovy diff --git a/xtraplatform-blobs/src/main/java/de/ii/xtraplatform/blobs/infra/BlobExtractorZip.java b/xtraplatform-blobs/src/main/java/de/ii/xtraplatform/blobs/infra/BlobExtractorZip.java index aefd6690a..78b834bb9 100644 --- a/xtraplatform-blobs/src/main/java/de/ii/xtraplatform/blobs/infra/BlobExtractorZip.java +++ b/xtraplatform-blobs/src/main/java/de/ii/xtraplatform/blobs/infra/BlobExtractorZip.java @@ -41,7 +41,16 @@ public void extract( return; } Path entry = archiveRoot.relativize(zipEntry); - Path target = targetRoot.resolve(entry); + Path target = targetRoot.resolve(entry).normalize(); + + // Zip-slip guard: an entry name with `..` segments must not write outside the target + // directory. + if (!target.startsWith(targetRoot.normalize())) { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn("Skipping zip entry outside the target directory: {}", zipEntry); + } + return; + } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Extracting to {} {}", target, includeEntry.test(entry)); diff --git a/xtraplatform-blobs/src/test/groovy/de/ii/xtraplatform/blobs/infra/BlobExtractorZipSpec.groovy b/xtraplatform-blobs/src/test/groovy/de/ii/xtraplatform/blobs/infra/BlobExtractorZipSpec.groovy new file mode 100644 index 000000000..dfc52737d --- /dev/null +++ b/xtraplatform-blobs/src/test/groovy/de/ii/xtraplatform/blobs/infra/BlobExtractorZipSpec.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2025 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.blobs.infra + +import spock.lang.Specification + +import java.nio.file.Files +import java.nio.file.Path +import java.util.function.Predicate +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +class BlobExtractorZipSpec extends Specification { + + def "a zip entry with .. is not written outside the target directory"() { + given: + Path base = Files.createTempDirectory("ziptest") + Path targetRoot = Files.createDirectory(base.resolve("out")) + Path zipFile = base.resolve("archive.zip") + new ZipOutputStream(Files.newOutputStream(zipFile)).withCloseable { zos -> + zos.putNextEntry(new ZipEntry("good.txt")) + zos.write("good".getBytes("UTF-8")) + zos.closeEntry() + zos.putNextEntry(new ZipEntry("../evil.txt")) + zos.write("evil".getBytes("UTF-8")) + zos.closeEntry() + } + + when: + new BlobExtractorZip().extract(zipFile, Path.of("/"), { true } as Predicate, targetRoot, true) + + then: + Files.exists(targetRoot.resolve("good.txt")) + !Files.exists(base.resolve("evil.txt")) + } +}