From 90efb8bdf6b33ee9b4bfc0db1153ce18c91679ca Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Wed, 30 Sep 2026 13:14:01 +0200 Subject: [PATCH] Fix `ISO8601Utils#parse` time zone validation --- .../internal/bind/DefaultDateTypeAdapter.java | 3 +- .../gson/internal/bind/util/ISO8601Utils.java | 31 ++++++++++- .../internal/bind/util/ISO8601UtilsTest.java | 52 ++++++++++++++++--- 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java index b5dffe24fb..52f68371b1 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java @@ -30,7 +30,6 @@ import java.io.IOException; import java.text.DateFormat; import java.text.ParseException; -import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -179,7 +178,7 @@ private Date deserializeToDate(JsonReader in) throws IOException { } try { - return ISO8601Utils.parse(s, new ParsePosition(0)); + return ISO8601Utils.parseFully(s); } catch (ParseException e) { throw new JsonSyntaxException( "Failed parsing '" + s + "' as Date; at path " + in.getPreviousPath(), e); diff --git a/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java b/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java index 606d6c7146..89772a6f04 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java +++ b/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java @@ -33,7 +33,7 @@ * * @see this specification */ -// Date parsing code from Jackson databind ISO8601Utils.java +// Date parsing code from Jackson databind ISO8601Utils.java, with modifications // https://github.com/FasterXML/jackson-databind/blob/2.8/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java @SuppressWarnings("MemberName") // legacy class name public final class ISO8601Utils { @@ -143,7 +143,11 @@ public static String format(Date date, boolean millis, TimeZone tz) { * @param pos The position to start parsing from, updated to where parsing stopped. * @return the parsed date * @throws ParseException if the date is not in the appropriate format + * @deprecated For Gson-internal usage prefer {@link #parseFully(String)}.
+ * For external users: {@code ISO8601Utils} is a Gson-internal class; prefer {@code java.time} + * for date parsing. */ + @Deprecated public static Date parse(String date, ParsePosition pos) throws ParseException { Exception fail = null; try { @@ -237,14 +241,18 @@ public static Date parse(String date, ParsePosition pos) throws ParseException { if (timezoneIndicator == 'Z') { timezone = TIMEZONE_UTC; offset += 1; + + if (date.length() > offset) { + throw new IllegalArgumentException("Trailing data after time zone 'Z'"); + } } else if (timezoneIndicator == '+' || timezoneIndicator == '-') { String timezoneOffset = date.substring(offset); + offset += timezoneOffset.length(); // When timezone has no minutes, we should append it, valid timezones are, for example: // +00:00, +0000 and +00 timezoneOffset = timezoneOffset.length() >= 5 ? timezoneOffset : timezoneOffset + "00"; - offset += timezoneOffset.length(); // 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00" if (timezoneOffset.equals("+0000") || timezoneOffset.equals("+00:00")) { timezone = TIMEZONE_UTC; @@ -308,6 +316,25 @@ public static Date parse(String date, ParsePosition pos) throws ParseException { throw ex; } + /** + * Fully parses a date string. + * + *

Delegates to {@link #parse(String, ParsePosition)} and verifies that the given string was + * fully parsed. + */ + public static Date parseFully(String dateStr) throws ParseException { + ParsePosition pos = new ParsePosition(0); + Date date = ISO8601Utils.parse(dateStr, pos); + + int parseEnd = pos.getIndex(); + if (parseEnd != dateStr.length()) { + // Note: Should be effectively unreachable because `ISO8601Utils#parse` already throws if + // there is unrelated trailing data; but keep this check here to be safe + throw new ParseException("Failed to fully parse date [" + dateStr + "]", parseEnd); + } + return date; + } + /** * Check if the expected character exist at the given offset in the value. * diff --git a/gson/src/test/java/com/google/gson/internal/bind/util/ISO8601UtilsTest.java b/gson/src/test/java/com/google/gson/internal/bind/util/ISO8601UtilsTest.java index 1c2d254dec..51e4792e8a 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/util/ISO8601UtilsTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/util/ISO8601UtilsTest.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertThrows; import java.text.ParseException; -import java.text.ParsePosition; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -79,7 +78,7 @@ public void testDateFormatWithTimezone() { @SuppressWarnings("UndefinedEquals") public void testDateParseWithDefaultTimezone() throws ParseException { String dateStr = "2018-06-25"; - Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0)); + Date date = ISO8601Utils.parseFully(dateStr); Date expectedDate = new GregorianCalendar(2018, Calendar.JUNE, 25).getTime(); assertThat(date).isEqualTo(expectedDate); } @@ -87,20 +86,20 @@ public void testDateParseWithDefaultTimezone() throws ParseException { @Test public void testDateParseInvalidDay() { String dateStr = "2022-12-33"; - assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0))); + assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); } @Test public void testDateParseInvalidMonth() { String dateStr = "2022-14-30"; - assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0))); + assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); } @Test @SuppressWarnings("UndefinedEquals") public void testDateParseWithTimezone() throws ParseException { String dateStr = "2018-06-25T00:00:00-03:00"; - Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0)); + Date date = ISO8601Utils.parseFully(dateStr); GregorianCalendar calendar = createUtcCalendar(); calendar.set(2018, Calendar.JUNE, 25, 3, 0); Date expectedDate = calendar.getTime(); @@ -111,16 +110,55 @@ public void testDateParseWithTimezone() throws ParseException { @SuppressWarnings("UndefinedEquals") public void testDateParseSpecialTimezone() throws ParseException { String dateStr = "2018-06-25T00:02:00-02:58"; - Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0)); + Date date = ISO8601Utils.parseFully(dateStr); GregorianCalendar calendar = createUtcCalendar(); calendar.set(2018, Calendar.JUNE, 25, 3, 0); Date expectedDate = calendar.getTime(); assertThat(date).isEqualTo(expectedDate); } + @Test + @SuppressWarnings("UndefinedEquals") + public void testDateParseShortZeroTimezone() throws ParseException { + // timezone "+00" is extended by parser to "+0000" + String dateStr = "2018-06-25T00:00:00+00"; + Date date = ISO8601Utils.parseFully(dateStr); + GregorianCalendar calendar = createUtcCalendar(); + calendar.set(2018, Calendar.JUNE, 25, 0, 0); + Date expectedDate = calendar.getTime(); + assertThat(date).isEqualTo(expectedDate); + } + @Test public void testDateParseInvalidTime() { String dateStr = "2018-06-25T61:60:62-03:00"; - assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0))); + assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); + } + + @Test + public void testDateParseInvalidZTimezone() { + String dateStr = "2018-06-25T00:02:00Z10"; + var e = assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); + assertThat(e) + .hasMessageThat() + .isEqualTo("Failed to parse date [\"" + dateStr + "\"]: Trailing data after time zone 'Z'"); + } + + @Test + public void testDateParseNoTimezoneIndicator() { + String dateStr = "2018-06-25T00:02:00"; + var e = assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); + assertThat(e) + .hasMessageThat() + .isEqualTo("Failed to parse date [\"" + dateStr + "\"]: No time zone indicator"); + } + + @Test + public void testDateParseInvalidTimezoneIndicator() { + String dateStr = "2018-06-25T00:02:00X"; + var e = assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr)); + assertThat(e) + .hasMessageThat() + .isEqualTo("Failed to parse date [\"" + dateStr + "\"]: Invalid time zone indicator 'X'"); } }