HMCL-dev/HMCL · critical · IOException

Unsecure path:

Error message

Unsecure path: 

What it means

Thrown while completing/updating a server modpack when a file path declared in the remote modpack manifest resolves outside the instance root directory. HMCL normalizes each manifest entry and requires it to stay under the instance root; otherwise the path is treated as a path-traversal (zip-slip style) attack and installation aborts. This protects against malicious manifests writing files anywhere on disk.

Solutions

  1. Inspect the modpack manifest (modpack.json / server.json) and fix or remove entries whose path contains '..' or is absolute.
  2. Re-download the modpack from a trusted source; the manifest may be corrupted or tampered with.
  3. If you are the pack author, regenerate the manifest so every path is relative and stays inside the instance directory.
  4. Report the pack to its maintainer if the path traversal looks intentional.

Example fix

// before (manifest entry)
{"path": "../../../evil.jar", ...}
// after
{"path": "mods/legit-mod.jar", ...}
Defensive patterns

Strategy: validation

Validate before calling

Path rootPath = instanceRoot.toAbsolutePath().normalize();
for (var file : manifest.getFiles()) {
    Path p = rootPath.resolve(file.getPath()).toAbsolutePath().normalize();
    if (!p.startsWith(rootPath))
        throw new IllegalArgumentException("Unsecure path: " + file.getPath());
}

Try / catch

try {
    task.execute();
} catch (IOException e) {
    if (e.getMessage().startsWith("Unsecure path:")) {
        // reject/treat modpack as malicious or corrupted
    }
}

Prevention

When it happens

Trigger: ServerModpackCompletionTask.execute() iterating remoteManifest.getFiles() encounters a FileInformation whose path contains '../' segments, an absolute path, or a symlink-resolved location that escapes rootPath after normalize().

Common situations: A maliciously or incorrectly crafted server modpack manifest with paths like '../../.minecraft/...'; manifests produced by broken exporter tools; editing manifest JSON by hand and introducing '..' segments.

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

Appendix: source

Thrown at HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java:174

        Path rootPath = instance.getInstanceRoot().toAbsolutePath().normalize();
        Map<String, ModpackConfiguration.FileInformation> files = manifest.getManifest().getFiles().stream()
                .collect(Collectors.toMap(ModpackConfiguration.FileInformation::getPath,
                        Function.identity()));

        Set<String> remoteFiles = remoteManifest.getFiles().stream().map(ModpackConfiguration.FileInformation::getPath)
                .collect(Collectors.toSet());

        Path runDirectory = instance.getRunDirectory().toAbsolutePath().normalize();
        Path modsDirectory = runDirectory.resolve("mods");

        int total = 0;
        // for files in new modpack
        for (ModpackConfiguration.FileInformation file : remoteManifest.getFiles()) {
            Path actualPath = rootPath.resolve(file.getPath()).toAbsolutePath().normalize();
            String fileName = actualPath.getFileName().toString();

            if (!actualPath.startsWith(rootPath)) {
                throw new IOException("Unsecure path: " + file.getPath());
            }

            boolean download;

            boolean isModDisabled = modsDirectory.equals(actualPath.getParent()) &&
                    (Files.exists(actualPath.resolveSibling(fileName + LocalAddonManager.DISABLED_EXTENSION)) ||
                            Files.exists(actualPath.resolveSibling(fileName + LocalAddonManager.OLD_EXTENSION)));

            if (isModDisabled) {
                download = false;
            } else if (!files.containsKey(file.getPath())) {
                // If old modpack does not have this entry, download it
                download = true;
            } else if (!Files.exists(actualPath)) {
                // If both old and new modpacks have this entry, but the file is missing...
                // Re-download it since network problem may cause file missing
                download = true;
            } else {

View on GitHub (pinned to 24702dc5a0)