HMCL-dev/HMCL · error

Zip entry is trying to write outside of the destination…

Error message

Zip entry is trying to write outside of the destination directory: 

What it means

Unzipper rejects zip entries whose normalized target path escapes destDir (zip-slip attack). It resolves the entry against the destination and verifies the result still starts with destDir; if not, an IOException naming the entry is thrown.

Solutions

  1. Obtain archives from trusted, verified sources and re-download a clean copy
  2. Inspect the archive (jar tf / unzip -l) for '..' or absolute-path entries and rebuild it if needed
  3. Keep the default Unzipper safety behavior — do not bypass the destDir containment check
  4. Extract to a dedicated scratch directory and validate contents before moving them into place

Example fix

// before
// archive contains entry: ../../../plugins/evil.jar
new Unzipper().zip(zipFile).destDirectory(pluginsDir).unzip();
// after
// rebuild the archive with relative paths only, then extract
new Unzipper().zip(zipFileFixed).destDirectory(pluginsDir).unzip();
Defensive patterns

Strategy: validation

Validate before calling

static boolean isInside(Path root, Path candidate) {
    return candidate.toAbsolutePath().normalize().startsWith(root.toAbsolutePath().normalize());
}
// pre-scan archive:
try (ZipFile zf = new ZipFile(zip)) {
    for (ZipEntry e : Collections.list(zf.entries()))
        if (e.getName().contains("..") || Paths.get(e.getName()).isAbsolute())
            throw new IOException("unsafe entry: " + e.getName());
}

Type guard

static boolean safeEntryName(String name) {
    Path p = Paths.get(name);
    return !p.isAbsolute() && !p.normalize().startsWith("..");
}

Try / catch

try {
    new Unzipper().zip(zip).destDirectory(dest).unzip();
} catch (IOException e) {
    if (e.getMessage().startsWith("Zip entry is trying to write outside")) {
        // reject the archive / warn user about unsafe content
    } else throw e;
}

Prevention

When it happens

Trigger: Extracting a maliciously or accidentally crafted archive containing entries like '../../evil.so' or absolute paths that resolve outside the destination directory.

Common situations: Installing mods/resource packs from untrusted third-party sites; archives built on Windows with backslash paths or '..' segments; corrupted archives from interrupted downloads.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10). Data as JSON: /api/errors/193616422d8e7b76. Report an issue: GitHub.

Appendix: source

Thrown at HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Unzipper.java:107

        CopyOption[] copyOptions = replaceExistentFile
                ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING}
                : new CopyOption[]{};

        long entryCount = 0L;
        try (ZipArchiveReader reader = CompressingUtils.openZipFileWithPossibleEncoding(zipFile, encoding)) {
            String pathPrefix = StringUtils.addSuffix(subDirectory, "/");

            for (ZipArchiveEntry entry : reader.getEntries()) {
                String normalizedPath = FileUtils.normalizePath(entry.getName());
                if (!normalizedPath.startsWith(pathPrefix)) {
                    continue;
                }

                String relativePath = normalizedPath.substring(pathPrefix.length());
                Path destFile = destDir.resolve(relativePath).toAbsolutePath().normalize();
                if (!destFile.startsWith(destDir)) {
                    throw new IOException("Zip entry is trying to write outside of the destination directory: " + entry.getName());
                }

                if (filter != null && !filter.accept(entry, destFile, relativePath)) {
                    continue;
                }

                entryCount++;

                if (entry.isDirectory()) {
                    Files.createDirectories(destFile);
                } else {
                    Files.createDirectories(destFile.getParent());
                    if (entry.isUnixSymlink()) {
                        String linkTarget = reader.getUnixSymlink(entry);
                        if (replaceExistentFile)
                            Files.deleteIfExists(destFile);

                        Path targetPath;

View on GitHub (pinned to 24702dc5a0)