Severity: MAJOR
File: JarRemoteClassSource.java
Problem
JarRemoteClassSource downloads entire JAR files into memory/disk with NO size validation. Can cause OOM or fill up disk with multi-GB JARs.
Bugs
1. Line 119: Unlimited JAR download size
try (InputStream in = connection.getInputStream()) {
Files.copy(in, tempJarPath, StandardCopyOption.REPLACE_EXISTING);
}
Problem: Downloads entire JAR file with no size check
Attack scenario:
- Attacker creates 10GB "JAR" file
- Points JClassLoader to malicious URL
- Files.copy() downloads entire 10GB to temp directory
- Fills up disk space
- System-wide failures when disk is full
Impact:
- Disk space exhaustion
- Temp directory fills up
- System-wide disruption
- Other applications fail when disk full
Fix: Check Content-Length before downloading:
private synchronized void ensureJarReady() throws IOException {
if (closed) {
throw new IllegalStateException("JarRemoteClassSource is closed");
}
if (jarFile != null) {
return;
}
tempJarPath = Files.createTempFile("jclassloader-jar-", ".jar");
retryPolicy.execute(() -> {
URL url = new URL(jarUrl);
URLConnection connection = url.openConnection();
connection.setConnectTimeout(connectTimeoutMs);
connection.setReadTimeout(readTimeoutMs);
if (connection instanceof HttpURLConnection) {
HttpURLConnection httpConnection = (HttpURLConnection) connection;
configureSSL(httpConnection);
configureAuthentication(httpConnection);
int responseCode = httpConnection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new IOException("HTTP error code: " + responseCode + " for URL: " + url);
}
// Check size BEFORE downloading
long contentLength = httpConnection.getContentLengthLong();
if (contentLength > MAX_JAR_SIZE) { // e.g., 100MB
throw new IOException(
"JAR file too large: " + contentLength + " bytes (max " + MAX_JAR_SIZE + ")"
);
}
}
try (InputStream in = connection.getInputStream()) {
Files.copy(in, tempJarPath, StandardCopyOption.REPLACE_EXISTING);
}
// Validate downloaded file size
long actualSize = Files.size(tempJarPath);
if (actualSize > MAX_JAR_SIZE) {
Files.deleteIfExists(tempJarPath);
throw new IOException(
"Downloaded JAR exceeds size limit: " + actualSize + " bytes"
);
}
return null;
});
// Open the JAR file
jarFile = new JarFile(tempJarPath.toFile());
}
Add MAX_JAR_SIZE constant:
private static final long MAX_JAR_SIZE = 100 * 1024 * 1024; // 100MB default
// Make it configurable via Builder pattern
public static class Builder {
private long maxJarSize = MAX_JAR_SIZE;
public Builder maxJarSize(long maxBytes) {
if (maxBytes <= 0) {
throw new IllegalArgumentException("Max JAR size must be > 0");
}
this.maxJarSize = maxBytes;
return this;
}
// ... other builder methods ...
}
2. Lines 140-149: No size validation on JAR entries
try (InputStream in = jarFile.getInputStream(entry);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
return out.toByteArray();
}
Problem: Reads entire JAR entry into ByteArrayOutputStream with no size check
Scenario: JAR contains a class file that is actually multi-GB of garbage data
- ByteArrayOutputStream allocates multi-GB
- OutOfMemoryError
Fix: Check entry size before reading:
@Override
public byte[] loadClassData(String className) throws IOException {
ensureJarReady();
String entryName = ClassNameUtil.toClassFilePath(className);
JarEntry entry = jarFile.getJarEntry(entryName);
if (entry == null) {
throw new IOException("Class not found in JAR: " + className);
}
long size = entry.getSize();
if (size < 0) {
// Unknown size - read with limit
return readWithSizeLimit(jarFile.getInputStream(entry), MAX_CLASS_SIZE);
}
if (size > MAX_CLASS_SIZE) { // e.g., 10MB
throw new IOException(
"Class file too large: " + size + " bytes (max " + MAX_CLASS_SIZE + ")"
);
}
if (size > Integer.MAX_VALUE) {
throw new IOException("Class file exceeds Java array limit: " + size);
}
try (InputStream in = jarFile.getInputStream(entry)) {
byte[] data = new byte[(int)size];
int totalRead = 0;
while (totalRead < size) {
int n = in.read(data, totalRead, (int)size - totalRead);
if (n == -1) break;
totalRead += n;
}
return data;
}
}
private byte[] readWithSizeLimit(InputStream in, long maxSize) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
long totalRead = 0;
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
totalRead += bytesRead;
if (totalRead > maxSize) {
throw new IOException("Entry exceeds maximum size: " + totalRead);
}
out.write(buffer, 0, bytesRead);
}
return out.toByteArray();
}
3. Lines 154-161: canLoad() triggers expensive JAR download
@Override
public boolean canLoad(String className) {
try {
ensureJarReady(); // ← Downloads entire JAR!
String entryName = ClassNameUtil.toClassFilePath(className);
return jarFile.getJarEntry(entryName) != null;
} catch (IOException e) {
return false;
}
}
Problem: First call to canLoad() downloads entire JAR
Scenario:
JarRemoteClassSource source = new JarRemoteClassSource("https://example.com/huge.jar");
boolean can = source.canLoad("com.example.Test"); // ← Downloads entire JAR!
This defeats the purpose of canLoad() being a "lightweight" check.
Impact:
- canLoad() is supposed to be fast
- ClassSource interface documentation says canLoad() "may be expensive for remote sources"
- But this is especially expensive - downloads ENTIRE JAR just to check one entry
Mitigation: This is actually acceptable behavior for JarRemoteClassSource because:
- JAR is downloaded once and cached
- Subsequent canLoad() calls are fast
- Alternative would be HTTP Range requests which are complex
Recommendation: Document this clearly:
/**
* Checks if this source can load the specified class.
*
* <p><b>Performance Note:</b> The first call to this method (or loadClassData())
* will download the entire JAR file. Subsequent calls use the cached JAR and are fast.
* The download is synchronized and happens only once per instance.</p>
*
* @param className The fully qualified class name to check
* @return true if the class exists in the JAR, false otherwise
*/
@Override
public boolean canLoad(String className) {
// ...
}
4. Lines 189-191: Swallows IOException on jarFile.close()
if (jarFile != null) {
try {
jarFile.close();
} catch (IOException e) {
// Continue to delete temp file
}
}
Problem: Silently swallows exception
Better approach: Log the exception
if (jarFile != null) {
try {
jarFile.close();
} catch (IOException e) {
logger.warn("Failed to close JAR file: {} - {}", jarUrl, e.getMessage());
}
}
Required Actions
- Add MAX_JAR_SIZE validation (default 100MB, configurable)
- Add MAX_CLASS_SIZE validation for JAR entries (default 10MB, configurable)
- Document canLoad() performance characteristics
- Log exception when jarFile.close() fails
Impact
Current code allows:
- Multi-GB JAR downloads filling up disk
- Multi-GB class entries causing OOM
- Silent failures in cleanup
With fixes:
- Bounded resource usage
- Clear error messages
- Better observability
Severity: MAJOR
File: JarRemoteClassSource.java
Problem
JarRemoteClassSource downloads entire JAR files into memory/disk with NO size validation. Can cause OOM or fill up disk with multi-GB JARs.
Bugs
1. Line 119: Unlimited JAR download size
Problem: Downloads entire JAR file with no size check
Attack scenario:
Impact:
Fix: Check Content-Length before downloading:
Add MAX_JAR_SIZE constant:
2. Lines 140-149: No size validation on JAR entries
Problem: Reads entire JAR entry into ByteArrayOutputStream with no size check
Scenario: JAR contains a class file that is actually multi-GB of garbage data
Fix: Check entry size before reading:
3. Lines 154-161: canLoad() triggers expensive JAR download
Problem: First call to canLoad() downloads entire JAR
Scenario:
This defeats the purpose of canLoad() being a "lightweight" check.
Impact:
Mitigation: This is actually acceptable behavior for JarRemoteClassSource because:
Recommendation: Document this clearly:
4. Lines 189-191: Swallows IOException on jarFile.close()
Problem: Silently swallows exception
Better approach: Log the exception
Required Actions
Impact
Current code allows:
With fixes: