This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 6b1decd76 WW-5813 fix(conversion): reject date input with text after 
the parsed value
6b1decd76 is described below

commit 6b1decd769c275b43086ba5b8a608adac84bc787
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sun Oct 25 06:27:27 2026 +0100

    WW-5813 fix(conversion): reject date input with text after the parsed value
    
    DateConverter parsed with SimpleDateFormat.parse(String), which stops at
    the end of the pattern and ignores the rest. A date-time whose separator
    did not match the locale's date-time pattern fell back to the date-only
    format and silently became midnight, and "24-10-2026 25:00:00" was
    accepted the same way. The final parse now has to consume the whole
    input, for java.util.Date, java.sql.Timestamp and java.sql.Time.
    
    That makes selectFormat's lenient fallback dead: whatever it picked had
    already failed the strict whole-input parse, so it is removed.
    
    ISO date-times ending in Z, with or without milliseconds (the form
    Date.toISOString() produces), used to convert only because the Z was
    ignored. Two explicit formats keep them converting with the same
    server-local reading as before; the milliseconds are now kept instead of
    dropped. Reading Z as UTC is WW-5814.
    
    testSqlTimeType and testSqlTimestampType relied on the ignored trailing
    text (" AM" under a 24-hour pattern on JDK 17, a date-only prefix match on
    JDK 25), so their inputs are now formatted with the locale's own pattern.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../struts2/conversion/impl/DateConverter.java     | 41 ++++++++---------
 .../struts2/conversion/impl/DateConverterTest.java | 51 ++++++++++++++++++++--
 2 files changed, 64 insertions(+), 28 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/conversion/impl/DateConverter.java 
b/core/src/main/java/org/apache/struts2/conversion/impl/DateConverter.java
index 9cc061a80..44d835782 100644
--- a/core/src/main/java/org/apache/struts2/conversion/impl/DateConverter.java
+++ b/core/src/main/java/org/apache/struts2/conversion/impl/DateConverter.java
@@ -28,7 +28,6 @@ import org.apache.struts2.conversion.TypeConversionException;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Member;
 import java.text.DateFormat;
-import java.text.ParseException;
 import java.text.ParsePosition;
 import java.text.SimpleDateFormat;
 import java.time.Instant;
@@ -111,20 +110,20 @@ public class DateConverter extends DefaultTypeConverter {
             if (df == null) {
                 df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
             }
-            try {
-                df.setLenient(false); // let's use strict parsing (XW-341)
-                result = df.parse(sa);
-                if (!(Date.class == toType)) {
-                    try {
-                        Constructor<?> constructor = 
toType.getConstructor(long.class);
-                        return constructor.newInstance(result.getTime());
-                    } catch (Exception e) {
-                        throw new TypeConversionException(
-                                "Couldn't create class " + toType + " using 
default (long) constructor", e);
-                    }
+            df.setLenient(false); // let's use strict parsing (XW-341)
+            ParsePosition position = new ParsePosition(0);
+            result = df.parse(sa, position);
+            if (result == null || position.getIndex() != sa.length()) {
+                throw new TypeConversionException("Could not parse date");
+            }
+            if (!(Date.class == toType)) {
+                try {
+                    Constructor<?> constructor = 
toType.getConstructor(long.class);
+                    return constructor.newInstance(result.getTime());
+                } catch (Exception e) {
+                    throw new TypeConversionException(
+                            "Couldn't create class " + toType + " using 
default (long) constructor", e);
                 }
-            } catch (ParseException e) {
-                throw new TypeConversionException("Could not parse date", e);
             }
 
         } else if (value instanceof Date date) {
@@ -136,7 +135,6 @@ public class DateConverter extends DefaultTypeConverter {
     /**
      * Picks the first format that strictly parses the whole input, so a 
format that only matches leniently, or only
      * matches a prefix, does not hide a later one that fits (an ISO date 
under a {@code dd-MM-y} locale, for example).
-     * Without such a format it falls back to the first lenient match, which 
the caller then re-parses strictly.
      */
     protected DateFormat selectFormat(DateFormat[] formats, String input) {
         for (DateFormat format : formats) {
@@ -144,13 +142,6 @@ public class DateConverter extends DefaultTypeConverter {
                 return format;
             }
         }
-        for (DateFormat format : formats) {
-            try {
-                format.parse(input);
-                return format;
-            } catch (ParseException ignore) {
-            }
-        }
         return null;
     }
 
@@ -234,14 +225,16 @@ public class DateConverter extends DefaultTypeConverter {
         DateFormat d3 = DateFormat.getDateInstance(DateFormat.LONG, locale);
 
         DateFormat rfc3339 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+        DateFormat rfc3339Z = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+        DateFormat rfc3339MillisZ = new 
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
         DateFormat rfc3339dateOnly = new SimpleDateFormat("yyyy-MM-dd");
 
         final DateFormat[] dateFormats;
 
         if (globalDateFormat == null) {
-            dateFormats = new DateFormat[] { dt1, dt2, dt3, rfc3339, d1, d2, 
d3, rfc3339dateOnly };
+            dateFormats = new DateFormat[] { dt1, dt2, dt3, rfc3339, rfc3339Z, 
rfc3339MillisZ, d1, d2, d3, rfc3339dateOnly };
         } else {
-            dateFormats = new DateFormat[] { globalDateFormat, dt1, dt2, dt3, 
rfc3339, d1, d2, d3, rfc3339dateOnly };
+            dateFormats = new DateFormat[] { globalDateFormat, dt1, dt2, dt3, 
rfc3339, rfc3339Z, rfc3339MillisZ, d1, d2, d3, rfc3339dateOnly };
         }
 
         return dateFormats;
diff --git 
a/core/src/test/java/org/apache/struts2/conversion/impl/DateConverterTest.java 
b/core/src/test/java/org/apache/struts2/conversion/impl/DateConverterTest.java
index 074cb4f5b..6f1e498e6 100644
--- 
a/core/src/test/java/org/apache/struts2/conversion/impl/DateConverterTest.java
+++ 
b/core/src/test/java/org/apache/struts2/conversion/impl/DateConverterTest.java
@@ -28,6 +28,7 @@ import org.apache.struts2.conversion.TypeConversionException;
 import java.sql.Time;
 import java.sql.Timestamp;
 import java.text.DateFormat;
+import java.text.SimpleDateFormat;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
@@ -43,7 +44,6 @@ public class DateConverterTest extends StrutsInternalTestCase 
{
     private String INPUT_WHEN_LONG_CONSTRUCTOR_STR;
     private final Locale mxLocale = new Locale("es_MX", "MX");
     private final static String RES_TIME_STAMP_STR = "2020-03-20 00:00:00.0";
-    private final static String TIME_01_59_10 = "01:59:10 AM";
     private final static String DATE_STR = "2020-03-20";
     private final static String DATE_CONVERTED = "Fri Mar 20 00:00:00";
     private final static String INVALID_DATE = "99/99/2010";
@@ -63,7 +63,7 @@ public class DateConverterTest extends StrutsInternalTestCase 
{
 
         ActionContext context = ActionContext.of().withLocale(mxLocale);
 
-        Object value = converter.convertValue(context.getContextMap(), null, 
null, null, TIME_01_59_10, Time.class);
+        Object value = converter.convertValue(context.getContextMap(), null, 
null, null, DateFormat.getTimeInstance(DateFormat.MEDIUM, 
mxLocale).format(Time.valueOf("01:59:10")), Time.class);
         assertEquals("01:59:10", value.toString());
     }
 
@@ -229,6 +229,49 @@ public class DateConverterTest extends 
StrutsInternalTestCase {
         assertEquals(Timestamp.valueOf("2026-10-24 10:15:30.123"), value);
     }
 
+    public void testDateWithTrailingTextIsRejected() {
+        try {
+            convertToDate("24-10-2026 at 10:15:30", DUTCH, null);
+            fail("TypeConversionException expected");
+        } catch (TypeConversionException e) {
+            assertEquals(MESSAGE_PARSE_ERROR, e.getMessage());
+        }
+    }
+
+    public void testIsoDateTimeWithATrailingZ() {
+        Object value = convertToDate("2026-10-24T10:15:30Z", Locale.US, null);
+
+        assertEquals(LocalDateTime.of(2026, 10, 24, 10, 15, 30), 
toLocalDateTime(value));
+    }
+
+    public void testIsoDateTimeWithMillisAndATrailingZ() {
+        Object value = convertToDate("2026-10-24T10:15:30.123Z", Locale.US, 
null);
+
+        assertEquals(LocalDateTime.of(2026, 10, 24, 10, 15, 30, 123_000_000), 
toLocalDateTime(value));
+    }
+
+    public void testSqlTimestampWithTrailingTextIsRejected() {
+        ActionContext context = ActionContext.of().withLocale(DUTCH);
+
+        try {
+            new DateConverter().convertValue(context.getContextMap(), null, 
null, null, "24-10-2026 at 10:15:30", Timestamp.class);
+            fail("TypeConversionException expected");
+        } catch (TypeConversionException e) {
+            assertEquals(MESSAGE_PARSE_ERROR, e.getMessage());
+        }
+    }
+
+    public void testSqlTimeWithTrailingTextIsRejected() {
+        ActionContext context = ActionContext.of().withLocale(DUTCH);
+
+        try {
+            new DateConverter().convertValue(context.getContextMap(), null, 
null, null, "10:15:30 tomorrow", Time.class);
+            fail("TypeConversionException expected");
+        } catch (TypeConversionException e) {
+            assertEquals(MESSAGE_PARSE_ERROR, e.getMessage());
+        }
+    }
+
     private Object convertToDate(String input, Locale locale, String 
globalFormat) {
         ValueStack stack = new StubValueStack();
         Map<String, String> texts = new HashMap<>();
@@ -295,12 +338,12 @@ public class DateConverterTest extends 
StrutsInternalTestCase {
         // Due to JEP 252: Use CLDR Locale Data by Default
         DateFormat dFormat = DateFormat.getDateInstance(DateFormat.SHORT, 
mxLocale);
         if (dFormat.format(new Date()).contains("-")) { // Format when Java 9 
or greater
-            INPUT_TIME_STAMP_STR = "2020-03-20 00:00:00.000";
             INPUT_WHEN_LONG_CONSTRUCTOR_STR = "2020-03-20";
         } else {// Format when Java 8 or lower
-            INPUT_TIME_STAMP_STR = "03/20/2020 00:00:00.000";
             INPUT_WHEN_LONG_CONSTRUCTOR_STR = "03/31/20";
         }
+        SimpleDateFormat dateTime = (SimpleDateFormat) 
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, mxLocale);
+        INPUT_TIME_STAMP_STR = new SimpleDateFormat(dateTime.toPattern() + 
".SSS", mxLocale).format(Timestamp.valueOf(RES_TIME_STAMP_STR));
     }
 
 }

Reply via email to