From e59a82cdc32cb0d277b1b2f7eea137a2f5399f6c Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Thu, 16 Jul 2026 23:26:39 +0530 Subject: [PATCH] reject trailing characters in strict date validation Parse with a ParsePosition and require the whole value to be consumed when strict, so a value with trailing text such as "11/11/199f" no longer validates against "MM/dd/yyyy". --- .../commons/validator/DateValidator.java | 12 +++++----- .../validator/GenericTypeValidator.java | 22 ++++++++----------- .../validator/GenericTypeValidatorTest.java | 15 +++++++++++++ .../validator/GenericValidatorTest.java | 8 +++++++ 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/apache/commons/validator/DateValidator.java b/src/main/java/org/apache/commons/validator/DateValidator.java index cecccecde..a65829855 100644 --- a/src/main/java/org/apache/commons/validator/DateValidator.java +++ b/src/main/java/org/apache/commons/validator/DateValidator.java @@ -18,6 +18,7 @@ import java.text.DateFormat; import java.text.ParseException; +import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Locale; @@ -97,12 +98,11 @@ public boolean isValid(final String value, final String datePattern, final boole } final SimpleDateFormat formatter = new SimpleDateFormat(datePattern); formatter.setLenient(false); - try { - formatter.parse(value); - } catch (final ParseException e) { - return false; - } - if (strict && datePattern.length() != value.length()) { + final ParsePosition pos = new ParsePosition(0); + // parse(String, ParsePosition) stops at the first unparsable character instead of failing, so a strict + // match must also confirm the whole value was consumed; otherwise trailing text such as the 'f' in + // "11/11/199f" is dropped and the truncated date validates. + if (formatter.parse(value, pos) == null || strict && (pos.getIndex() < value.length() || datePattern.length() != value.length())) { return false; } return true; diff --git a/src/main/java/org/apache/commons/validator/GenericTypeValidator.java b/src/main/java/org/apache/commons/validator/GenericTypeValidator.java index 62fe3f44b..4c4134f37 100644 --- a/src/main/java/org/apache/commons/validator/GenericTypeValidator.java +++ b/src/main/java/org/apache/commons/validator/GenericTypeValidator.java @@ -143,22 +143,18 @@ public static Date formatDate(final String value, final Locale locale) { * @return The converted Date value. */ public static Date formatDate(final String value, final String datePattern, final boolean strict) { - Date date = null; if (value == null || datePattern == null || datePattern.isEmpty()) { return null; } - try { - final SimpleDateFormat formatter = new SimpleDateFormat(datePattern); - formatter.setLenient(false); - date = formatter.parse(value); - if (strict && datePattern.length() != value.length()) { - date = null; - } - } catch (final ParseException e) { - // Bad date so return null - if (LOG.isDebugEnabled()) { - LOG.debug("Date parse failed value=[" + value + "], " + "pattern=[" + datePattern + "], " + "strict=[" + strict + "] " + e); - } + final SimpleDateFormat formatter = new SimpleDateFormat(datePattern); + formatter.setLenient(false); + final ParsePosition pos = new ParsePosition(0); + Date date = formatter.parse(value, pos); + // parse(String, ParsePosition) stops at the first unparsable character instead of failing, so a strict + // match must also confirm the whole value was consumed; otherwise trailing text such as the 'f' in + // "11/11/199f" is dropped and the truncated date validates. + if (date != null && strict && (pos.getIndex() < value.length() || datePattern.length() != value.length())) { + date = null; } return date; } diff --git a/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java b/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java index 1a2be5245..99329278f 100644 --- a/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/GenericTypeValidatorTest.java @@ -133,6 +133,21 @@ void testLongLocaleOverflow() { assertNull(GenericTypeValidator.formatLong("123x", Locale.US)); } + /** + * Tests that strict {@link GenericTypeValidator#formatDate(String, String, boolean)} rejects a value with trailing characters instead of parsing only its + * leading portion. + */ + @Test + void testFormatDateStrict() { + assertNotNull(GenericTypeValidator.formatDate("11/11/1999", "MM/dd/yyyy", true)); + // The trailing 'f' used to be dropped, leaving the year parsed as 199 and the value reported as valid. + assertNull(GenericTypeValidator.formatDate("11/11/199f", "MM/dd/yyyy", true)); + // An abbreviated field is still rejected in strict mode. + assertNull(GenericTypeValidator.formatDate("2/12/1999", "MM/dd/yyyy", true)); + // Non-strict parsing stays lenient about the pattern length. + assertNotNull(GenericTypeValidator.formatDate("2/12/1999", "MM/dd/yyyy", false)); + } + /** * Tests the byte validation. */ diff --git a/src/test/java/org/apache/commons/validator/GenericValidatorTest.java b/src/test/java/org/apache/commons/validator/GenericValidatorTest.java index 1b27bc33f..36b81fe29 100644 --- a/src/test/java/org/apache/commons/validator/GenericValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/GenericValidatorTest.java @@ -48,6 +48,14 @@ void testMaxLength() { assertTrue(GenericValidator.maxLength("12345\n\r", 7, 2), "Max=7 End=2"); } + @Test + void testIsDate() { + assertTrue(GenericValidator.isDate("11/11/1999", "MM/dd/yyyy", true), "valid strict date"); + // Strict validation used to accept a value with a trailing character by parsing only its leading portion. + assertFalse(GenericValidator.isDate("11/11/199f", "MM/dd/yyyy", true), "trailing character"); + assertFalse(GenericValidator.isDate("2/12/1999", "MM/dd/yyyy", true), "abbreviated month"); + } + @Test void testMinLength() {