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));
}
}