@@ -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