From 91f2d591bc8eb42c14ef2c1b3cb91c5571cadaac Mon Sep 17 00:00:00 2001 From: bibi samina Date: Sat, 11 Jul 2026 12:26:22 +0530 Subject: [PATCH 1/3] validate locale subtags in GwtLocale fromString Reject any subtag that is not 1-8 alphanumeric characters at the point the locale string is split. The split only breaks on '-'/'_', so characters like '.', '$' or '/' otherwise survive in the primary language subtag, which flows into class names resolved reflectively by LocalizableInstantiator when server code calls GWT.create on a Localizable with an untrusted 'locale' property. --- .../gwt/i18n/server/GwtLocaleFactoryImpl.java | 21 ++++++++++++++++++ .../google/gwt/i18n/server/GwtLocaleTest.java | 22 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/user/src/com/google/gwt/i18n/server/GwtLocaleFactoryImpl.java b/user/src/com/google/gwt/i18n/server/GwtLocaleFactoryImpl.java index 3af2f5a6d25..1fa64d4d5f3 100644 --- a/user/src/com/google/gwt/i18n/server/GwtLocaleFactoryImpl.java +++ b/user/src/com/google/gwt/i18n/server/GwtLocaleFactoryImpl.java @@ -36,6 +36,19 @@ private static boolean isDigit(String str, int min, int max) { return matches(str, min, max, false); } + private static boolean isAlphaNumeric(String str, int min, int max) { + int len = str.length(); + if (len < min || len > max) { + return false; + } + for (int i = 0; i < len; ++i) { + if (!Character.isLetterOrDigit(str.charAt(i))) { + return false; + } + } + return true; + } + /** * Check if the supplied string matches length and composition requirements. * @@ -143,6 +156,14 @@ public GwtLocale fromString(String localeName) { ArrayList localeParts = new ArrayList(); String[] parts = localeName.split("[-_]"); for (int i = 0; i < parts.length; ++i) { + // The split only breaks on '-'/'_', so any other character stays inside + // a part. Require each subtag to be alphanumeric and 1-8 chars, matching + // BCP47, so an untrusted server-side locale can't smuggle '.', '$' or + // '/' into the class name LocalizableInstantiator resolves reflectively. + if (!isAlphaNumeric(parts[i], 1, 8)) { + throw new IllegalArgumentException("Unrecognized locale format: " + + localeName); + } if (parts[i].length() == 1 && i + 1 < parts.length) { localeParts.add(parts[i] + '-' + parts[++i]); } else { diff --git a/user/test/com/google/gwt/i18n/server/GwtLocaleTest.java b/user/test/com/google/gwt/i18n/server/GwtLocaleTest.java index 6b3e101ff8d..8e5cd48401d 100644 --- a/user/test/com/google/gwt/i18n/server/GwtLocaleTest.java +++ b/user/test/com/google/gwt/i18n/server/GwtLocaleTest.java @@ -174,6 +174,28 @@ public void testFromString() { } } + public void testFromStringRejectsInvalidLanguage() { + // The language subtag is concatenated into class names resolved reflectively + // on the server, so it must not carry characters like '.', '$' or '/'. + String[] invalid = { + "com.google.gwt.dev.Compiler", "java.lang.Runtime", "x$Evil", "a/b/c", + "en.US", + }; + for (String locale : invalid) { + try { + factory.fromString(locale); + fail("Should have thrown IllegalArgumentException on " + locale); + } catch (IllegalArgumentException expected) { + } + } + // Well-formed tags, including extended-language and private-use forms, are + // still accepted with the language preserved verbatim. + assertEquals("en", factory.fromString("en_US").getLanguage()); + assertEquals("zh-cmn", factory.fromString("zh-cmn").getLanguage()); + assertEquals("i-klingon", factory.fromString("i-klingon").getLanguage()); + assertEquals("x-foo123", factory.fromString("x-foo123").getLanguage()); + } + public void testInheritance() { GwtLocale en = factory.fromString("en_Latn_US_VARIANT"); List chain = en.getInheritanceChain(); From 5f52b8b74cc910901e1557a2207cb89ee6749fc7 Mon Sep 17 00:00:00 2001 From: bibi samina Date: Thu, 8 Oct 2026 15:04:03 +0530 Subject: [PATCH 2/3] restrict subtag validation to ASCII and assert via assertThrows Use explicit ASCII ranges in isAlphaNumeric so the BCP47 check no longer accepts non-ASCII letters and digits that Character.isLetterOrDigit permits. Replace the empty catch in the new test with an assertThrows helper in com.google.gwt.testing.server.Assertions. --- .../gwt/i18n/server/GwtLocaleFactoryImpl.java | 7 ++- .../google/gwt/i18n/server/GwtLocaleTest.java | 10 ++-- .../google/gwt/testing/server/Assertions.java | 52 +++++++++++++++++++ 3 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 user/test/com/google/gwt/testing/server/Assertions.java diff --git a/user/src/com/google/gwt/i18n/server/GwtLocaleFactoryImpl.java b/user/src/com/google/gwt/i18n/server/GwtLocaleFactoryImpl.java index 1fa64d4d5f3..af14f7bae60 100644 --- a/user/src/com/google/gwt/i18n/server/GwtLocaleFactoryImpl.java +++ b/user/src/com/google/gwt/i18n/server/GwtLocaleFactoryImpl.java @@ -42,7 +42,12 @@ private static boolean isAlphaNumeric(String str, int min, int max) { return false; } for (int i = 0; i < len; ++i) { - if (!Character.isLetterOrDigit(str.charAt(i))) { + char c = str.charAt(i); + // BCP47 subtags are ASCII only, so avoid Character.isLetterOrDigit which + // would also accept non-ASCII letters and digits. + boolean alphaNumeric = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9'); + if (!alphaNumeric) { return false; } } diff --git a/user/test/com/google/gwt/i18n/server/GwtLocaleTest.java b/user/test/com/google/gwt/i18n/server/GwtLocaleTest.java index 8e5cd48401d..e7e98f75d5c 100644 --- a/user/test/com/google/gwt/i18n/server/GwtLocaleTest.java +++ b/user/test/com/google/gwt/i18n/server/GwtLocaleTest.java @@ -15,6 +15,8 @@ */ package com.google.gwt.i18n.server; +import static com.google.gwt.testing.server.Assertions.assertThrows; + import com.google.gwt.i18n.shared.GwtLocale; import com.google.gwt.i18n.shared.GwtLocaleFactory; @@ -182,11 +184,9 @@ public void testFromStringRejectsInvalidLanguage() { "en.US", }; for (String locale : invalid) { - try { - factory.fromString(locale); - fail("Should have thrown IllegalArgumentException on " + locale); - } catch (IllegalArgumentException expected) { - } + assertThrows(IllegalArgumentException.class, + () -> factory.fromString(locale), + "Should have thrown IllegalArgumentException on " + locale); } // Well-formed tags, including extended-language and private-use forms, are // still accepted with the language preserved verbatim. diff --git a/user/test/com/google/gwt/testing/server/Assertions.java b/user/test/com/google/gwt/testing/server/Assertions.java new file mode 100644 index 00000000000..c54c7aef11c --- /dev/null +++ b/user/test/com/google/gwt/testing/server/Assertions.java @@ -0,0 +1,52 @@ +/* + * 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.testing.server; + +/** + * Assertion helpers for server-side JUnit 3 tests, where the JUnit 4/5 + * {@code assertThrows} is not available. + */ +public class Assertions { + + /** + * A block of code that may throw any {@link Throwable}, so it can be passed to + * {@link #assertThrows} as a lambda. + */ + public interface Executable { + void execute() throws Throwable; + } + + /** + * Asserts that running {@code executable} throws an exception of + * {@code expectedType} (or a subtype) and returns it, failing with + * {@code message} if nothing is thrown or a different type is thrown. + */ + public static T assertThrows(Class expectedType, + Executable executable, String message) { + try { + executable.execute(); + } catch (Throwable actual) { + if (expectedType.isInstance(actual)) { + return expectedType.cast(actual); + } + throw new AssertionError(message + " but threw " + actual, actual); + } + throw new AssertionError(message); + } + + private Assertions() { + } +} From b1fd6049e460e936b441e86116a0be1c08f7ed42 Mon Sep 17 00:00:00 2001 From: bibi samina Date: Fri, 9 Oct 2026 11:51:51 +0530 Subject: [PATCH 3/3] exclude JVM-only test helpers from translatable source Update the copyright header on Assertions to GWT Project Authors. Exclude the com.google.gwt.testing.server subpackage from the TestUtils module's source path so the assertThrows helper keeps Class.isInstance/cast and is no longer picked up by the GWT compiler. --- user/test/com/google/gwt/testing/TestUtils.gwt.xml | 6 +++++- user/test/com/google/gwt/testing/server/Assertions.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/user/test/com/google/gwt/testing/TestUtils.gwt.xml b/user/test/com/google/gwt/testing/TestUtils.gwt.xml index d485f79fbe8..1ebd70e2653 100644 --- a/user/test/com/google/gwt/testing/TestUtils.gwt.xml +++ b/user/test/com/google/gwt/testing/TestUtils.gwt.xml @@ -13,5 +13,9 @@ - + + + + diff --git a/user/test/com/google/gwt/testing/server/Assertions.java b/user/test/com/google/gwt/testing/server/Assertions.java index c54c7aef11c..f70d1c8dc3a 100644 --- a/user/test/com/google/gwt/testing/server/Assertions.java +++ b/user/test/com/google/gwt/testing/server/Assertions.java @@ -1,5 +1,5 @@ /* - * Copyright 2026 Google Inc. + * Copyright 2026 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