From 64729b6e1d23fae5f37f3e489d1e1b2b05e4c2e3 Mon Sep 17 00:00:00 2001 From: metsw24-max Date: Thu, 9 Apr 2026 12:00:42 +0530 Subject: [PATCH 1/2] Harden Accept-Encoding parsing to prevent NPE --- dev/codeserver/BUILD | 9 +++ .../google/gwt/dev/codeserver/WebServer.java | 51 ++++++++++++++++- .../dev/codeserver/WebServerSecurityTest.java | 56 +++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerSecurityTest.java diff --git a/dev/codeserver/BUILD b/dev/codeserver/BUILD index 76d4db72791..0331a19f769 100644 --- a/dev/codeserver/BUILD +++ b/dev/codeserver/BUILD @@ -64,6 +64,7 @@ test_suite( tests = [ ":RecompilerTest", ":SourceHandlerTest", + ":WebServerSecurityTest", ], ) @@ -83,6 +84,14 @@ java_test( ], ) +java_test( + name = "WebServerSecurityTest", + test_class = "com.google.gwt.dev.codeserver.WebServerSecurityTest", + runtime_deps = [ + ":testlib", + ], +) + # Repackaged codeserver for google3. AugmentedJar( name = "codeserver", diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java index b9deeca1387..a8a59448afc 100644 --- a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java +++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java @@ -39,6 +39,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; @@ -372,7 +373,7 @@ public void send(HttpServletRequest request, HttpServletResponse response, TreeL } if (contentEncoding != null) { - if (!request.getHeader("Accept-Encoding").contains("gzip")) { + if (!acceptsGzipEncoding(request.getHeader("Accept-Encoding"))) { response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); logger.log(TreeLogger.WARN, "client doesn't accept gzip; bailing"); return; @@ -543,6 +544,54 @@ static String guessMimeType(String filename) { return mimeType != null ? mimeType : ""; } + /* visible for testing */ + static boolean acceptsGzipEncoding(String acceptEncodingHeader) { + if (acceptEncodingHeader == null || acceptEncodingHeader.trim().isEmpty()) { + return false; + } + + Double gzipQValue = null; + Double wildcardQValue = null; + + for (String encodingSpec : acceptEncodingHeader.split(",")) { + String[] parts = encodingSpec.trim().split(";"); + if (parts.length == 0) { + continue; + } + + String encoding = parts[0].trim().toLowerCase(Locale.ROOT); + if (encoding.isEmpty()) { + continue; + } + + double qValue = 1.0; + for (int i = 1; i < parts.length; i++) { + String parameter = parts[i].trim().toLowerCase(Locale.ROOT); + if (parameter.startsWith("q=")) { + String qValueText = parameter.substring("q=".length()).trim(); + try { + qValue = Double.parseDouble(qValueText); + } catch (NumberFormatException e) { + qValue = 0.0; + } + break; + } + } + + if (encoding.equals("gzip")) { + gzipQValue = qValue; + } else if (encoding.equals("*")) { + wildcardQValue = qValue; + } + } + + if (gzipQValue != null) { + return gzipQValue > 0.0; + } + + return wildcardQValue != null && wildcardQValue > 0.0; + } + /** * Returns the binding properties from the web page where dev mode is being used. (As passed in * by dev_mode_on.js in a JSONP request to "/recompile".) diff --git a/dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerSecurityTest.java b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerSecurityTest.java new file mode 100644 index 00000000000..0edad79f1b5 --- /dev/null +++ b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerSecurityTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.dev.codeserver; + +import junit.framework.TestCase; + +/** + * Security-focused tests for request header parsing in {@link WebServer}. + */ +public class WebServerSecurityTest extends TestCase { + + public void testAcceptsGzipEncodingRejectsNullOrEmptyHeader() { + assertFalse(WebServer.acceptsGzipEncoding(null)); + assertFalse(WebServer.acceptsGzipEncoding("")); + assertFalse(WebServer.acceptsGzipEncoding(" ")); + } + + public void testAcceptsGzipEncodingAcceptsSimpleGzip() { + assertTrue(WebServer.acceptsGzipEncoding("gzip")); + assertTrue(WebServer.acceptsGzipEncoding("deflate, gzip")); + assertTrue(WebServer.acceptsGzipEncoding("GZIP")); + } + + public void testAcceptsGzipEncodingRejectsExplicitGzipZeroQValue() { + assertFalse(WebServer.acceptsGzipEncoding("gzip;q=0")); + assertFalse(WebServer.acceptsGzipEncoding("deflate, gzip; q=0.0")); + } + + public void testAcceptsGzipEncodingHonorsWildcardWhenGzipAbsent() { + assertTrue(WebServer.acceptsGzipEncoding("*")); + assertTrue(WebServer.acceptsGzipEncoding("br;q=0.2, *;q=0.7")); + assertFalse(WebServer.acceptsGzipEncoding("*;q=0")); + } + + public void testAcceptsGzipEncodingPrefersExplicitGzipOverWildcard() { + assertFalse(WebServer.acceptsGzipEncoding("gzip;q=0, *;q=1")); + assertTrue(WebServer.acceptsGzipEncoding("gzip;q=1, *;q=0")); + } + + public void testAcceptsGzipEncodingRejectsMalformedQValue() { + assertFalse(WebServer.acceptsGzipEncoding("gzip;q=not-a-number")); + } +} From 21240b7fa4aa2e06c4d8b539d1228f68c0ebfaa6 Mon Sep 17 00:00:00 2001 From: metsw24-max Date: Thu, 9 Apr 2026 20:42:01 +0530 Subject: [PATCH 2/2] correctness and RFC 9110 compliance for Accept-Encoding handling --- dev/codeserver/BUILD | 9 ---- .../google/gwt/dev/codeserver/WebServer.java | 41 ++++++++----------- ....java => WebServerAcceptEncodingTest.java} | 26 +++++++++--- 3 files changed, 38 insertions(+), 38 deletions(-) rename dev/codeserver/javatests/com/google/gwt/dev/codeserver/{WebServerSecurityTest.java => WebServerAcceptEncodingTest.java} (66%) diff --git a/dev/codeserver/BUILD b/dev/codeserver/BUILD index 0331a19f769..76d4db72791 100644 --- a/dev/codeserver/BUILD +++ b/dev/codeserver/BUILD @@ -64,7 +64,6 @@ test_suite( tests = [ ":RecompilerTest", ":SourceHandlerTest", - ":WebServerSecurityTest", ], ) @@ -84,14 +83,6 @@ java_test( ], ) -java_test( - name = "WebServerSecurityTest", - test_class = "com.google.gwt.dev.codeserver.WebServerSecurityTest", - runtime_deps = [ - ":testlib", - ], -) - # Repackaged codeserver for google3. AugmentedJar( name = "codeserver", diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java index a8a59448afc..20f95999457 100644 --- a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java +++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java @@ -85,6 +85,10 @@ public class WebServer { private static final Pattern CACHE_JS_FILE = Pattern.compile("/(" + STRONG_NAME + ").cache.js$"); + private static final Pattern ACCEPT_ENCODING_SPEC = Pattern.compile( + "^\\s*([!#$%&'*+.^_`|~0-9A-Za-z-]+|\\*)\\s*(?:;\\s*q\\s*=\\s*" + + "(0(?:\\.\\d{0,3})?|1(?:\\.0{0,3})?))?\\s*$"); + private static final MimeTypes MIME_TYPES = new MimeTypes(); private static final String TIME_IN_THE_PAST = "Mon, 01 Jan 1990 00:00:00 GMT"; @@ -374,8 +378,10 @@ public void send(HttpServletRequest request, HttpServletResponse response, TreeL if (contentEncoding != null) { if (!acceptsGzipEncoding(request.getHeader("Accept-Encoding"))) { - response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); - logger.log(TreeLogger.WARN, "client doesn't accept gzip; bailing"); + response.setHeader("Accept-Encoding", "gzip"); + response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + logger.log(TreeLogger.WARN, + "client doesn't accept gzip and no uncompressed representation exists; bailing"); return; } response.setHeader("Content-Encoding", "gzip"); @@ -546,7 +552,11 @@ static String guessMimeType(String filename) { /* visible for testing */ static boolean acceptsGzipEncoding(String acceptEncodingHeader) { - if (acceptEncodingHeader == null || acceptEncodingHeader.trim().isEmpty()) { + if (acceptEncodingHeader == null) { + // RFC 9110: if Accept-Encoding is absent, any content coding is acceptable. + return true; + } + if (acceptEncodingHeader.trim().isEmpty()) { return false; } @@ -554,29 +564,14 @@ static boolean acceptsGzipEncoding(String acceptEncodingHeader) { Double wildcardQValue = null; for (String encodingSpec : acceptEncodingHeader.split(",")) { - String[] parts = encodingSpec.trim().split(";"); - if (parts.length == 0) { - continue; - } - - String encoding = parts[0].trim().toLowerCase(Locale.ROOT); - if (encoding.isEmpty()) { + Matcher matcher = ACCEPT_ENCODING_SPEC.matcher(encodingSpec); + if (!matcher.matches()) { continue; } - double qValue = 1.0; - for (int i = 1; i < parts.length; i++) { - String parameter = parts[i].trim().toLowerCase(Locale.ROOT); - if (parameter.startsWith("q=")) { - String qValueText = parameter.substring("q=".length()).trim(); - try { - qValue = Double.parseDouble(qValueText); - } catch (NumberFormatException e) { - qValue = 0.0; - } - break; - } - } + String encoding = matcher.group(1).toLowerCase(Locale.ROOT); + String qValueText = matcher.group(2); + double qValue = qValueText == null ? 1.0 : Double.parseDouble(qValueText); if (encoding.equals("gzip")) { gzipQValue = qValue; diff --git a/dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerSecurityTest.java b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerAcceptEncodingTest.java similarity index 66% rename from dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerSecurityTest.java rename to dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerAcceptEncodingTest.java index 0edad79f1b5..e35e0e516c1 100644 --- a/dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerSecurityTest.java +++ b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/WebServerAcceptEncodingTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2026 Google Inc. + * Copyright 2026 The GWT Project Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -18,12 +18,15 @@ import junit.framework.TestCase; /** - * Security-focused tests for request header parsing in {@link WebServer}. + * Tests request Accept-Encoding parsing for serving gzip-compressed responses. */ -public class WebServerSecurityTest extends TestCase { +public class WebServerAcceptEncodingTest extends TestCase { - public void testAcceptsGzipEncodingRejectsNullOrEmptyHeader() { - assertFalse(WebServer.acceptsGzipEncoding(null)); + public void testAcceptsGzipEncodingWhenHeaderIsAbsent() { + assertTrue(WebServer.acceptsGzipEncoding(null)); + } + + public void testAcceptsGzipEncodingRejectsEmptyHeaderValue() { assertFalse(WebServer.acceptsGzipEncoding("")); assertFalse(WebServer.acceptsGzipEncoding(" ")); } @@ -34,6 +37,11 @@ public void testAcceptsGzipEncodingAcceptsSimpleGzip() { assertTrue(WebServer.acceptsGzipEncoding("GZIP")); } + public void testAcceptsGzipEncodingRejectsSubstringMatches() { + assertFalse(WebServer.acceptsGzipEncoding("xgzip")); + assertFalse(WebServer.acceptsGzipEncoding("gzip-alt")); + } + public void testAcceptsGzipEncodingRejectsExplicitGzipZeroQValue() { assertFalse(WebServer.acceptsGzipEncoding("gzip;q=0")); assertFalse(WebServer.acceptsGzipEncoding("deflate, gzip; q=0.0")); @@ -50,7 +58,13 @@ public void testAcceptsGzipEncodingPrefersExplicitGzipOverWildcard() { assertTrue(WebServer.acceptsGzipEncoding("gzip;q=1, *;q=0")); } - public void testAcceptsGzipEncodingRejectsMalformedQValue() { + public void testAcceptsGzipEncodingRejectsInvalidQualityValues() { assertFalse(WebServer.acceptsGzipEncoding("gzip;q=not-a-number")); + assertFalse(WebServer.acceptsGzipEncoding("gzip;q=1.1")); + assertFalse(WebServer.acceptsGzipEncoding("gzip;q=0.1234")); + } + + public void testAcceptsGzipEncodingRejectsUnsupportedParameters() { + assertFalse(WebServer.acceptsGzipEncoding("gzip;level=9")); } }