HMCL-dev/HMCL · error

Zip entry is trying to create a symlink outside of the…

Error message

Zip entry is trying to create a symlink outside of the destination directory: 

What it means

For symlink entries, Unzipper resolves the link target relative to the entry's destination parent and checks it stays inside destDir; if the resolved target escapes the destination it throws an IOException (symlink-based path traversal defense).

Solutions

  1. Use archives from trusted sources and repack without out-of-tree symlinks
  2. Replace absolute symlinks with relative ones that stay inside the archive root
  3. Extract with the built-in containment check enabled (default) and review offending entries listed in the message
  4. Remove unneeded symlink entries from the package entirely

Example fix

// before
// archive entry: usr -> /usr  (absolute, escapes destination)
// after
// archive entry: usr/lib -> ../lib  (relative, stays inside destDir)
Defensive patterns

Strategy: validation

Validate before calling

static boolean symlinkStaysInside(Path destDir, Path entryDest, String linkTarget) {
    return entryDest.getParent().resolve(linkTarget).toAbsolutePath().normalize()
        .startsWith(destDir.toAbsolutePath().normalize());
}

Try / catch

try {
    unzipper.unzip();
} catch (IOException e) {
    if (e.getMessage().startsWith("Zip entry is trying to create a symlink outside")) {
        // treat archive as untrusted; reject and report
    } else throw e;
}

Prevention

When it happens

Trigger: Extracting archives containing symlinks whose targets (absolute paths or many '../' segments) resolve outside the extraction root, e.g. link -> /etc or link -> ../../somewhere.

Common situations: Malicious archives designed to plant symlinks for later write-escape attacks; archives meant to be installed system-wide being unpacked into a game directory; mistakenly packaged absolute symlinks from build machines.

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/e9fe511542514074. Report an issue: GitHub.

Appendix: source

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

                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;
                        try {
                            targetPath = Path.of(linkTarget);
                        } catch (InvalidPathException e) {
                            throw new IOException("Zip entry has an invalid symlink target: " + entry.getName(), e);
                        }

                        if (!destFile.getParent().resolve(targetPath).toAbsolutePath().normalize().startsWith(destDir)) {
                            throw new IOException("Zip entry is trying to create a symlink outside of the destination directory: " + entry.getName());
                        }

                        try {
                            Files.createSymbolicLink(destFile, targetPath);
                        } catch (FileAlreadyExistsException ignored) {
                        }
                    } else {
                        try (InputStream input = reader.getInputStream(entry)) {
                            Files.copy(input, destFile, copyOptions);
                        } catch (FileAlreadyExistsException e) {
                            if (replaceExistentFile)
                                throw e;
                        }

                        if (entry.getUnixMode() != 0 && OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS) {
                            Files.setPosixFilePermissions(destFile, FileUtils.parsePosixFilePermission(entry.getUnixMode()));
                        }
                    }

View on GitHub (pinned to 24702dc5a0)