BcelWeaverOutputPathTest.java
/* *******************************************************************
* Copyright (c) 2026 Contributors
* All rights reserved.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License v 2.0
* which accompanies this distribution and is available at
* https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
* ******************************************************************/
package org.aspectj.weaver.bcel;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.util.FileUtil;
import junit.framework.TestCase;
public class BcelWeaverOutputPathTest extends TestCase {
private File tempDirectory;
private File outDir;
private File maliciousJar;
protected void setUp() throws Exception {
super.setUp();
tempDirectory = File.createTempFile("bcelweaveroutputpathtest", "root");
tempDirectory.delete();
tempDirectory.mkdirs();
outDir = new File(tempDirectory, "out");
outDir.mkdirs();
byte[] bytes;
try (InputStream is = getClass().getResourceAsStream(getClass().getSimpleName() + ".class")) {
bytes = FileUtil.readAsByteArray(is);
}
// Create a rogue jar with a relative path in it
maliciousJar = new File(tempDirectory, "bad.jar");
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(maliciousJar))) {
jos.putNextEntry(new JarEntry("../escaped.class"));
jos.write(bytes);
jos.closeEntry();
}
}
protected void tearDown() throws Exception {
deleteRecursively(tempDirectory);
super.tearDown();
}
private static void deleteRecursively(File f) {
File[] children = f.listFiles();
if (children != null) {
for (File c : children) {
deleteRecursively(c);
}
}
f.delete();
}
private File createTypicalJar() throws IOException {
File jar = new File(tempDirectory, "safe.jar");
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar))) {
jos.putNextEntry(new JarEntry("META-INF/resource.txt"));
jos.write("helloworld".getBytes());
jos.closeEntry();
}
return jar;
}
private File createJarWithEntry(String entryName) throws IOException {
File jar = new File(tempDirectory, "single.jar");
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar))) {
jos.putNextEntry(new JarEntry(entryName));
jos.write("data".getBytes());
jos.closeEntry();
}
return jar;
}
private List<IMessage> addJarToWeaver(File jar) {
CollectingMessageHandler messageHandler = new CollectingMessageHandler();
BcelWeaver weaver = new BcelWeaver(new BcelWorld(new ArrayList<String>(), messageHandler, null));
weaver.addJarFile(jar, outDir, false);
return messageHandler.messages;
}
private void assertEntryAccepted(String entryName) throws IOException {
List<IMessage> messages = addJarToWeaver(createJarWithEntry(entryName));
assertTrue("expected '" + entryName + "' to be accepted, got: " + messages, messages.isEmpty());
}
private void assertEntryRejected(String entryName, String expectedReason) throws IOException {
List<IMessage> messages = addJarToWeaver(createJarWithEntry(entryName));
assertEquals("expected '" + entryName + "' to be rejected, got: " + messages, 1, messages.size());
String text = messages.get(0).getMessage();
assertTrue("expected message to name the entry, got: " + text, text.contains(entryName));
assertTrue("expected message to contain '" + expectedReason + "', got: " + text, text.contains(expectedReason));
}
private static class CollectingMessageHandler implements IMessageHandler {
final List<IMessage> messages = new ArrayList<>();
public boolean handleMessage(IMessage message) {
messages.add(message);
return true;
}
public boolean isIgnoring(IMessage.Kind kind) {
return false;
}
public void dontIgnore(IMessage.Kind kind) {
}
public void ignore(IMessage.Kind kind) {
}
}
public void testNullOutputDirectoryIsTolerated() throws IOException {
CollectingMessageHandler messageHandler = new CollectingMessageHandler();
BcelWorld world = new BcelWorld(new ArrayList<String>(), messageHandler, null);
BcelWeaver weaver = new BcelWeaver(world);
List<UnwovenClassFile> added = weaver.addJarFile(createTypicalJar(), null, false);
assertTrue("no class entries in the jar, so nothing should be added", added.isEmpty());
assertTrue("expected no messages, got: " + messageHandler.messages, messageHandler.messages.isEmpty());
}
public void testMaliciousJarContentsCannotResolveOutsideOutputDirectory() throws IOException {
CollectingMessageHandler messageHandler = new CollectingMessageHandler();
BcelWorld world = new BcelWorld(new ArrayList<String>(), messageHandler, null);
BcelWeaver weaver = new BcelWeaver(world);
List<UnwovenClassFile> added = weaver.addJarFile(maliciousJar, outDir, false);
assertTrue("expected the malicious entry to be rejected, not added", added.isEmpty());
assertEquals("expected error message about the rejected entry", 1, messageHandler.messages.size());
String messageText = messageHandler.messages.get(0).getMessage();
assertTrue("expected message to name the offending entry, got: " + messageText,
messageText.contains("escaped.class"));
assertTrue("expected message to explain why, got: " + messageText,
messageText.contains("outside destination directory"));
assertFalse("entry must not have escaped the output directory",
new File(tempDirectory, "escaped.class").exists());
}
public void testNormalNestedEntryIsAccepted() throws IOException {
assertEntryAccepted("META-INF/resource.txt");
}
public void testEntryThatNormalizesInsideOutDirIsAccepted() throws IOException {
assertEntryAccepted("a/../b.txt");
}
public void testEntryResolvingToOutDirItselfIsAccepted() throws IOException {
// pins the equality case now covered by Path.startsWith
assertEntryAccepted("a/..");
}
public void testEntryTraversingAboveOutDirIsRejected() throws IOException {
assertEntryRejected("a/../../x.txt", "outside destination directory");
}
public void testSiblingDirectorySharingPrefixIsRejected() throws IOException {
// outDir is <temp>/out, so ../out2/x.txt is a string-prefix match but not a child
assertEntryRejected("../out2/x.txt", "outside destination directory");
}
public void testAbsoluteEntryIsRejected() throws IOException {
// Path.resolve() replaces the base for an absolute argument
assertEntryRejected("/abs/x.txt", "outside destination directory");
}
public void testEntryWithNulCharacterIsReportedNotThrown() throws IOException {
// InvalidPathException is unchecked; it must surface as the normal error message
assertEntryRejected("bad\u0000name.txt", "not a valid path");
}
}