Skip to content

Commit 197be9a

Browse files
fix(security): prevent SSRF via URL scheme allowlist on openConnection calls
- Add safeOpenConnection() gatekeeper that restricts URL schemes to file: and jar: before opening any connection - Validate inner URL scheme for jar: references to block jar:http:// bypass vectors - Route all three openConnection() call sites in lookupNoCache() through the new validation helper Signed-off-by: manticore-projects <andreas@manticore-projects.com>
1 parent 95ba2aa commit 197be9a

File tree

1 file changed

+42
-3
lines changed

1 file changed

+42
-3
lines changed

webswing-server/webswing-server-api/src/main/java/org/webswing/server/api/services/resources/AbstractResourceHandler.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ protected LookupResult lookupNoCache(HttpServletRequest req, String path) {
227227
URL brUrl = webResourceProvider.getWebResource(path + ".br");
228228
if (brUrl != null) {
229229
try {
230-
return new PreCompressedResourceUrl(mimeType, brUrl.openConnection(), "br");
230+
return new PreCompressedResourceUrl(mimeType, safeOpenConnection(brUrl), "br");
231231
} catch (IOException e) {
232232
log.error("Failed to serve pre-compressed brotli for {}", sanitizeForLog(path), e);
233233
}
@@ -237,7 +237,7 @@ protected LookupResult lookupNoCache(HttpServletRequest req, String path) {
237237
URL gzUrl = webResourceProvider.getWebResource(path + ".gz");
238238
if (gzUrl != null) {
239239
try {
240-
return new PreCompressedResourceUrl(mimeType, gzUrl.openConnection(), "gzip");
240+
return new PreCompressedResourceUrl(mimeType, safeOpenConnection(gzUrl), "gzip");
241241
} catch (IOException e) {
242242
log.error("Failed to serve pre-compressed gzip for {}", sanitizeForLog(path), e);
243243
}
@@ -246,7 +246,7 @@ protected LookupResult lookupNoCache(HttpServletRequest req, String path) {
246246
}
247247

248248
try {
249-
return new ResourceUrl(mimeType, url.openConnection());
249+
return new ResourceUrl(mimeType, safeOpenConnection(url));
250250
} catch (IOException e) {
251251
log.error("Failed to serve path {} with resource {}", sanitizeForLog(path), sanitizeForLog(url.toString()), e);
252252
// Return a generic error message instead of the raw exception message
@@ -329,6 +329,45 @@ private static boolean isUnsafeRedirectPath(String path) {
329329
return path.startsWith("/") || path.contains("://") || path.startsWith("\\");
330330
}
331331

332+
/**
333+
* Allowlist of URL schemes that are safe to open connections to.
334+
* Only local resource schemes are permitted; network schemes like
335+
* http, https, ftp, gopher etc. are rejected to prevent SSRF.
336+
*/
337+
private static final java.util.Set<String> SAFE_URL_SCHEMES = java.util.Set.of("file", "jar");
338+
339+
/**
340+
* Open a connection to the given URL after validating that its scheme
341+
* is on the allowlist. This prevents Server-Side Request Forgery (SSRF)
342+
* when the URL originates from user-influenced resource lookups.
343+
*
344+
* @throws IOException if the scheme is not allowed or the connection fails
345+
*/
346+
private static URLConnection safeOpenConnection(URL url) throws IOException {
347+
String scheme = url.getProtocol();
348+
if (scheme == null || !SAFE_URL_SCHEMES.contains(scheme.toLowerCase(java.util.Locale.ROOT))) {
349+
throw new IOException("Blocked connection to disallowed URL scheme: " + scheme);
350+
}
351+
// For jar: URLs, verify the nested URL also uses a safe scheme
352+
if ("jar".equalsIgnoreCase(scheme)) {
353+
String spec = url.toString(); // jar:file:/path!/entry
354+
int bangIdx = spec.indexOf("!/");
355+
if (bangIdx > 0) {
356+
String inner = spec.substring(4, bangIdx); // strip "jar:"
357+
try {
358+
URL innerUrl = new URL(inner);
359+
String innerScheme = innerUrl.getProtocol();
360+
if (innerScheme == null || !SAFE_URL_SCHEMES.contains(innerScheme.toLowerCase(java.util.Locale.ROOT))) {
361+
throw new IOException("Blocked jar entry with disallowed inner URL scheme: " + innerScheme);
362+
}
363+
} catch (java.net.MalformedURLException e) {
364+
throw new IOException("Malformed inner URL in jar reference", e);
365+
}
366+
}
367+
}
368+
return url.openConnection();
369+
}
370+
332371
/**
333372
* Sanitize a string for safe inclusion in log messages.
334373
*/

0 commit comments

Comments
 (0)