Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/org/jetbrains/java/decompiler/struct/ContextUnit.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import org.jetbrains.java.decompiler.main.extern.IResultSaver;
import org.jetbrains.java.decompiler.util.TextBuffer;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
Expand Down Expand Up @@ -131,12 +133,14 @@ public void save(final Function<String, StructClass> loader) throws IOException

// directory entries
for (String dirEntry : dirEntries) {
if (dirEntry.contains("..")) { continue; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So hot take, with VF's goals as an analysis tool, our hazards are a bit non-traditional. While there's some concerns about an untrusted archive being able to write outside of the output directory (more difficult to do anything with when the working dir is unknown), the probably more pertinent one would be overwriting class files within the tree to mask the behaviour of the application. You don't want to ignore those entries but between emitting a warning or finding some directory to put them in, there's a few options.

This is probably too far up the abstractions to filter things -- I'm thinking DirectoryResultSaver would be better, basically where files go from being in a jar to being extracted to the outside FS to be a bit more defensive, but there's more work to square the output generated from VF with the way the JVM does class resolution (i.e. what files in the jar are actually loaded when a certain named class/resource is requested), while ensuring that even incorrectly located resources can be decompiled and analyzed.

VF's handling of malformed archives is pretty piecemeal, it deserves a more thorough look through to avoid hazards to operators' systems while still making sure they have visibility into any pecularities the archives they're analyzing might have.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

, but there's more work to square the output generated from VF with the way the JVM does class resolution (i.e. what files in the jar are actually loaded when a certain named class/resource is requested), while ensuring that even incorrectly located resources can be decompiled and analyzed.

Are you saying vineflower does dynamic resolution of java classes and is not only vulnerable to zipslip but loading malicious classes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, i'm referring to the way classloading in the analyzed jar would behave if the jar were to actually be executed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OH phew 😅

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I've gone ahead and move the changes to DirectoryResultSaver instead. Not sure if you wanted to add any additional reporting if you see shenangians here. But I did add a defensive check to make certain any files written indeed start with the expected root path after normalization.

sink.acceptDirectory(dirEntry);
}

// non-class entries
for (IContextSource.Entry otherEntry : otherEntries) {
sink.acceptOther(otherEntry.path());
String cleanedPath = FileSystems.getDefault().getPath("/"+otherEntry.path()).normalize().toString();
sink.acceptOther(cleanedPath.substring(1)); // remove initial / needed for normalize to work
}

//Whooo threads!
Expand Down
29 changes: 29 additions & 0 deletions test/org/jetbrains/java/decompiler/CommandLineTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static org.jetbrains.java.decompiler.DecompilerTestFixture.assertFilesEqual;
import static org.junit.jupiter.api.Assertions.assertFalse;

public class CommandLineTest {
private DecompilerTestFixture fixture;
Expand Down Expand Up @@ -48,6 +53,30 @@ public void testJarToDir() {
assertFilesEqual(fixture.getTestDataDir().resolve("bulk"), fixture.getTempDir().resolve("bulk_out"));
}

@Test
public void testMaliciousJarToDir() throws IOException {
String out = fixture.getTempDir().resolve("bulk_out").toAbsolutePath().toString();
String in = fixture.getTestDataDir().resolve("mal.jar").toAbsolutePath().toString();
// create malicious jar if doesn't exist already
if (!new File(in).exists())
{
String badFileName = new String("../../../../../../../../../../../tmp/test.txt");
try (ZipOutputStream maliciousOut = new ZipOutputStream(new FileOutputStream(fixture.getTestDataDir().resolve("mal.jar").toAbsolutePath().toString())))
{
ZipEntry entry = new ZipEntry(badFileName);
maliciousOut.putNextEntry(entry);
maliciousOut.write("womp womp".getBytes());
maliciousOut.closeEntry();
}
}

ConsoleDecompiler.main(new String[]{in, out});

TextBuffer.checkLeaks();
File f = new File("/tmp/test.txt");
Copy link

@wagyourtail wagyourtail Mar 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest using a more distinct file name... I could just have a /tmp/test.txt already on my system. also should probably have a windows version of the test too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JUnit also has a way to generate a test-specific temp file directory, that's probably the way to go

assertFalse(f.exists());
}

@Test
public void testZipToJar() {
String out = fixture.getTempDir().resolve("bulk_out.jar").toAbsolutePath().toString();
Expand Down