-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Fix zip slip check in StructContext on Windows #3357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Fix zip slip check in StructContext on Windows #3357
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR addresses a Windows-specific bug in the zip slip protection mechanism within StructContext.addArchive. The issue occurs when File.getCanonicalPath() is called on synthetic filesystem paths (combining ZIP file paths with entry names), which causes IOException on Windows and incorrectly marks valid ZIP archives as corrupted. The fix replaces the filesystem-based check with a string-based validation approach.
Key Changes:
- Replaced
File.getCanonicalPath()check with string normalization and pattern matching - Changed from constructing synthetic filesystem paths to directly validating ZIP entry names
- Preserved the security goal of preventing zip slip attacks while fixing Windows compatibility
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| File test = new File(file.getAbsolutePath(), name); | ||
| if (!test.getCanonicalPath().startsWith(file.getCanonicalPath() + File.separator)) { // check for zip slip exploit | ||
| String normalizedName = name.replace('\\', '/'); | ||
| if (normalizedName.startsWith("/") || normalizedName.startsWith(".." + "/") || normalizedName.contains("/" + ".." + "/")) { // check for zip slip exploit |
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The string concatenation using the + operator should be replaced with string literals for better performance and readability. Instead of ".." + "/" and "/" + ".." + "/", use "../" and "/../" directly. While the compiler may optimize simple concatenations, using string literals is clearer and avoids any potential runtime concatenation overhead.
| if (normalizedName.startsWith("/") || normalizedName.startsWith(".." + "/") || normalizedName.contains("/" + ".." + "/")) { // check for zip slip exploit | |
| if (normalizedName.startsWith("/") || normalizedName.startsWith("../") || normalizedName.contains("/../")) { // check for zip slip exploit |
| if (normalizedName.startsWith("/") || normalizedName.startsWith(".." + "/") || normalizedName.contains("/" + ".." + "/")) { // check for zip slip exploit | ||
| throw new RuntimeException("Zip entry '" + entry.getName() + "' tries to escape target directory"); | ||
| } |
Copilot
AI
Dec 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The zip slip check may not catch all path traversal attempts. Consider these edge cases:
- Entry names that are exactly
..(would not be caught bystartsWith("../")) - Entry names ending with
/..likesomedir/..(would not be caught bycontains("/../")) - Entry names ending with
..without a slash likefoo/bar..should be allowed, butfoo/bar/..should be rejected
The check should handle these cases to be more comprehensive. Consider using a pattern that checks for .. as a complete path component, such as checking if the normalized name equals ".." or checking for the pattern "/.." at the end of the string or followed by /.
| if (normalizedName.startsWith("/") || normalizedName.startsWith(".." + "/") || normalizedName.contains("/" + ".." + "/")) { // check for zip slip exploit | |
| throw new RuntimeException("Zip entry '" + entry.getName() + "' tries to escape target directory"); | |
| } | |
| if (normalizedName.startsWith("/")) { // check for zip slip exploit: absolute path | |
| throw new RuntimeException("Zip entry '" + entry.getName() + "' tries to escape target directory"); | |
| } | |
| for (String component : normalizedName.split("/")) { // check for zip slip exploit: parent directory traversal | |
| if ("..".equals(component)) { | |
| throw new RuntimeException("Zip entry '" + entry.getName() + "' tries to escape target directory"); | |
| } | |
| } |
|
Hi, could you please squash your changes and create a bug report at https://youtrack.jetbrains.com/issues/IDEA |
Summary
This PR fixes a Windows-specific bug in
StructContext.addArchivethat prevents Fernflower from decompiling any JAR/ZIP archives when running on Windows.The issue is not caused by corrupted archives – it is triggered by the zip-slip protection using
File.getCanonicalPath()on paths that are not real directories butzipFile\entrycombinations. On Windows this can throw anIOExceptioneven for perfectly valid entries, which then causes Fernflower to treat the whole archive as "corrupted".The fix replaces the filesystem-based zip-slip check with a string-based check on the normalized entry name. This preserves the security goal (rejecting absolute paths and
..escapes) while avoiding Windows-specific path resolution bugs.Problem / Error message
On Windows, any decompilation that involves archives inside an input directory fails with:
This happens not only for complex plugin archives, but can be reproduced with a minimal example:
a.zipcontaining a single empty fileb(no subdirectories).a.zip.StructContext.addArchivethrows the aboveIOExceptionfromgetCanonicalPath(), and the archive is reported as "Corrupted archive file".If the file
bis removed from the ZIP (so the ZIP is technically valid but empty), the error disappears. The problem is therefore unrelated to the ZIP structure itself and entirely due to how the path check is implemented.This also means that on Windows Fernflower cannot reliably "convert" lib JARs/ZIPs into decompiled versions: as soon as a problematic entry hits this check, processing aborts with the above error.
Root cause
The relevant code in
StructContext.addArchivecurrently does:Here,
fileis the ZIP/JAR itself, e.g.:For an entry
name = "b"(or e.g."TAX/b"), this constructs:Calling
getCanonicalPath()on such a "zipFile\entry" path is not meaningful in this context (Fernflower does not actually extract entries to disk), and on Windows this can fail with:This
IOExceptionbubbles up to theaddSpacemethod and is logged as:so the entire archive is discarded even though:
The current zip-slip protection therefore has two problems:
zipFile\entryfilesystem paths, which are not the actual extraction target.File.getCanonicalPath()for these pseudo-paths, which is fragile and platform-specific (fails on Windows for combinations that are perfectly valid as ZIP entries).Fernflower only reads entries from the archive, it does not actually extract them to the filesystem under
file.getAbsolutePath(), so using canonical filesystem paths here is not necessary.Fix
The fix keeps the idea of rejecting suspicious entry paths (zip slip protection), but changes the implementation to a normalized string-based check that does not depend on filesystem resolution:
Everything else in
addArchiveremains unchanged:.classentries are still loaded and decompiled as before.otherEntry/dirEntry.On Windows, the crucial difference is:
File.getCanonicalPath()onzipFile\entrypaths.IOExceptionis thrown for valid ZIP entries."Corrupted archive file"errors are processed successfully.Security considerations
The original intention of the code is to prevent zip-slip style path traversal attacks. In this context:
file.getAbsolutePath()..javafiles into the configured destination viaIResultSaver.Given that, the important safety property is:
The new check still enforces exactly this:
/(absolute paths),../,/../in the middle.com/example/Foo.class,b,TAX/b, etc.This preserves the zip-slip protection at the logical entry-name level, while removing the dependency on platform-specific canonical path resolution that caused valid archives to fail on Windows.
In other words:
zipFile\entryfilesystem path.Impact
plugins/directories).File.getCanonicalPath()for paths that are not real extraction targets.