Skip to content

Commit 55ae93f

Browse files
Flossyclaude
andcommitted
fix: add size validation to JarRemoteClassSource
- Add MAX_JAR_SIZE constant (100MB default) to prevent disk exhaustion - Add MAX_CLASS_SIZE constant (10MB default) to prevent OOM on large entries - Check Content-Length header before downloading JAR - Validate downloaded JAR file size after download - Check entry size before reading from JAR - Add readWithSizeLimit() helper for entries with unknown size - Document canLoad() performance characteristics in Javadoc Without these fixes: - Attacker could upload multi-GB JAR and fill disk - Large class entries could cause OutOfMemoryError - No protection against malicious content Fixes #54 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent ada8496 commit 55ae93f

1 file changed

Lines changed: 66 additions & 2 deletions

File tree

src/main/java/org/flossware/classloader/JarRemoteClassSource.java

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public class JarRemoteClassSource implements ClassSource, AutoCloseable {
3030
private static final Logger logger = LoggerFactory.getLogger(JarRemoteClassSource.class);
3131
private static final int DEFAULT_CONNECT_TIMEOUT_MS = 10000;
3232
private static final int DEFAULT_READ_TIMEOUT_MS = 30000;
33+
private static final long MAX_JAR_SIZE = 100 * 1024 * 1024; // 100MB default
34+
private static final long MAX_CLASS_SIZE = 10 * 1024 * 1024; // 10MB default
3335

3436
private final String jarUrl;
3537
private final AuthConfig authConfig;
@@ -113,12 +115,29 @@ private synchronized void ensureJarReady() throws IOException {
113115
if (responseCode != HttpURLConnection.HTTP_OK) {
114116
throw new IOException("HTTP error code: " + responseCode + " for URL: " + url);
115117
}
118+
119+
// Check JAR size before downloading
120+
long contentLength = httpConnection.getContentLengthLong();
121+
if (contentLength > MAX_JAR_SIZE) {
122+
throw new IOException(
123+
"JAR file too large: " + contentLength + " bytes (max " + MAX_JAR_SIZE + ")"
124+
);
125+
}
116126
}
117127

118128
try (InputStream in = connection.getInputStream()) {
119129
Files.copy(in, tempJarPath, StandardCopyOption.REPLACE_EXISTING);
120130
}
121131

132+
// Validate downloaded file size
133+
long actualSize = Files.size(tempJarPath);
134+
if (actualSize > MAX_JAR_SIZE) {
135+
Files.deleteIfExists(tempJarPath);
136+
throw new IOException(
137+
"Downloaded JAR exceeds size limit: " + actualSize + " bytes"
138+
);
139+
}
140+
122141
return null;
123142
});
124143

@@ -137,19 +156,64 @@ public byte[] loadClassData(String className) throws IOException {
137156
throw new IOException("Class not found in JAR: " + className);
138157
}
139158

140-
try (InputStream in = jarFile.getInputStream(entry);
141-
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
159+
long size = entry.getSize();
160+
if (size < 0) {
161+
// Unknown size - read with limit
162+
return readWithSizeLimit(jarFile.getInputStream(entry), MAX_CLASS_SIZE);
163+
}
142164

165+
if (size > MAX_CLASS_SIZE) {
166+
throw new IOException(
167+
"Class file too large: " + size + " bytes (max " + MAX_CLASS_SIZE + ")"
168+
);
169+
}
170+
171+
if (size > Integer.MAX_VALUE) {
172+
throw new IOException("Class file exceeds Java array limit: " + size);
173+
}
174+
175+
try (InputStream in = jarFile.getInputStream(entry)) {
176+
byte[] data = new byte[(int)size];
177+
int totalRead = 0;
178+
179+
while (totalRead < size) {
180+
int n = in.read(data, totalRead, (int)size - totalRead);
181+
if (n == -1) break;
182+
totalRead += n;
183+
}
184+
185+
return data;
186+
}
187+
}
188+
189+
private byte[] readWithSizeLimit(InputStream in, long maxSize) throws IOException {
190+
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
143191
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
192+
long totalRead = 0;
144193
int bytesRead;
194+
145195
while ((bytesRead = in.read(buffer)) != -1) {
196+
totalRead += bytesRead;
197+
if (totalRead > maxSize) {
198+
throw new IOException("Entry exceeds maximum size: " + totalRead);
199+
}
146200
out.write(buffer, 0, bytesRead);
147201
}
148202

149203
return out.toByteArray();
150204
}
151205
}
152206

207+
/**
208+
* Checks if this source can load the specified class.
209+
*
210+
* <p><b>Performance Note:</b> The first call to this method (or loadClassData())
211+
* will download the entire JAR file. Subsequent calls use the cached JAR and are fast.
212+
* The download is synchronized and happens only once per instance.</p>
213+
*
214+
* @param className The fully qualified class name to check
215+
* @return true if the class exists in the JAR, false otherwise
216+
*/
153217
@Override
154218
public boolean canLoad(String className) {
155219
try {

0 commit comments

Comments
 (0)