{"record":{"id":"193616422d8e7b76","repo":"HMCL-dev/HMCL","slug":"zip-entry-is-trying-to-write-outside-of-the-destin","errorCode":null,"errorMessage":"Zip entry is trying to write outside of the destination directory: ","messagePattern":"Zip entry is trying to write outside of the destination directory: ","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Unzipper.java","lineNumber":107,"sourceCode":"\n        CopyOption[] copyOptions = replaceExistentFile\n                ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING}\n                : new CopyOption[]{};\n\n        long entryCount = 0L;\n        try (ZipArchiveReader reader = CompressingUtils.openZipFileWithPossibleEncoding(zipFile, encoding)) {\n            String pathPrefix = StringUtils.addSuffix(subDirectory, \"/\");\n\n            for (ZipArchiveEntry entry : reader.getEntries()) {\n                String normalizedPath = FileUtils.normalizePath(entry.getName());\n                if (!normalizedPath.startsWith(pathPrefix)) {\n                    continue;\n                }\n\n                String relativePath = normalizedPath.substring(pathPrefix.length());\n                Path destFile = destDir.resolve(relativePath).toAbsolutePath().normalize();\n                if (!destFile.startsWith(destDir)) {\n                    throw new IOException(\"Zip entry is trying to write outside of the destination directory: \" + entry.getName());\n                }\n\n                if (filter != null && !filter.accept(entry, destFile, relativePath)) {\n                    continue;\n                }\n\n                entryCount++;\n\n                if (entry.isDirectory()) {\n                    Files.createDirectories(destFile);\n                } else {\n                    Files.createDirectories(destFile.getParent());\n                    if (entry.isUnixSymlink()) {\n                        String linkTarget = reader.getUnixSymlink(entry);\n                        if (replaceExistentFile)\n                            Files.deleteIfExists(destFile);\n\n                        Path targetPath;","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/HMCL-dev/HMCL/blob/24702dc5a0214034f4c27166d5fd30cad08cec19/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Unzipper.java#L89-L125","documentation":"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.","triggerScenarios":"Extracting a maliciously or accidentally crafted archive containing entries like '../../evil.so' or absolute paths that resolve outside the destination directory.","commonSituations":"Installing mods/resource packs from untrusted third-party sites; archives built on Windows with backslash paths or '..' segments; corrupted archives from interrupted downloads.","solutions":["Obtain archives from trusted, verified sources and re-download a clean copy","Inspect the archive (jar tf / unzip -l) for '..' or absolute-path entries and rebuild it if needed","Keep the default Unzipper safety behavior — do not bypass the destDir containment check","Extract to a dedicated scratch directory and validate contents before moving them into place"],"exampleFix":"// before\n// archive contains entry: ../../../plugins/evil.jar\nnew Unzipper().zip(zipFile).destDirectory(pluginsDir).unzip();\n// after\n// rebuild the archive with relative paths only, then extract\nnew Unzipper().zip(zipFileFixed).destDirectory(pluginsDir).unzip();","handlingStrategy":"validation","validationCode":"static boolean isInside(Path root, Path candidate) {\n    return candidate.toAbsolutePath().normalize().startsWith(root.toAbsolutePath().normalize());\n}\n// pre-scan archive:\ntry (ZipFile zf = new ZipFile(zip)) {\n    for (ZipEntry e : Collections.list(zf.entries()))\n        if (e.getName().contains(\"..\") || Paths.get(e.getName()).isAbsolute())\n            throw new IOException(\"unsafe entry: \" + e.getName());\n}","typeGuard":"static boolean safeEntryName(String name) {\n    Path p = Paths.get(name);\n    return !p.isAbsolute() && !p.normalize().startsWith(\"..\");\n}","tryCatchPattern":"try {\n    new Unzipper().zip(zip).destDirectory(dest).unzip();\n} catch (IOException e) {\n    if (e.getMessage().startsWith(\"Zip entry is trying to write outside\")) {\n        // reject the archive / warn user about unsafe content\n    } else throw e;\n}","preventionTips":["Only extract archives from trusted, checksum-verified sources","Pre-scan entry names for '..' and absolute paths","Extract untrusted archives into disposable scratch directories","Keep Unzipper's path containment check enabled"],"tags":["io","zip","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"24702dc5a0214034f4c27166d5fd30cad08cec19","analyzedAt":"2026-09-10T12:36:46.680Z","contentChangedAt":"2026-09-10T12:36:46.680Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}