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

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new abadb44f80 Interpret rfc850-date two-digit years as required by RFC 
9110
abadb44f80 is described below

commit abadb44f8087a6087608a333fd76fe7ec36769f9
Author: Arturo Bernal <[email protected]>
AuthorDate: Fri Oct 23 17:40:10 2026 +0200

    Interpret rfc850-date two-digit years as required by RFC 9110
    
    Section 5.6.7 of RFC 9110 requires a recipient to treat a two-digit year
    that appears to be more than 50 years in the future as the most recent
    year in the past with the same last two digits. FastHttpDateFormat used
    the default SimpleDateFormat window, which runs from 80 years ago to 20
    years ahead, so a year that is between 20 and 50 years ahead was read as
    a past year. Resolve two-digit years to the 100 year period that starts
    50 years ago.
---
 .../tomcat/util/http/ConcurrentDateFormat.java     | 29 +++++++++++++++++
 .../tomcat/util/http/FastHttpDateFormat.java       | 18 ++++++++++-
 .../tomcat/util/http/TestConcurrentDateFormat.java | 11 +++++++
 .../tomcat/util/http/TestFastHttpDateFormat.java   | 37 ++++++++++++++++++++++
 4 files changed, 94 insertions(+), 1 deletion(-)

diff --git a/java/org/apache/tomcat/util/http/ConcurrentDateFormat.java 
b/java/org/apache/tomcat/util/http/ConcurrentDateFormat.java
index ce38872686..8acddd241e 100644
--- a/java/org/apache/tomcat/util/http/ConcurrentDateFormat.java
+++ b/java/org/apache/tomcat/util/http/ConcurrentDateFormat.java
@@ -88,6 +88,35 @@ public class ConcurrentDateFormat {
         }
     }
 
+    /**
+     * Parses the given string into a date using the configured format 
pattern. A two-digit year is resolved to the
+     * 100 year period that starts at the given date.
+     *
+     * @param source the string to parse
+     * @param twoDigitYearStart the start of the 100 year period that a 
two-digit year is resolved to
+     *
+     * @return the parsed date
+     *
+     * @throws ParseException if the string cannot be parsed
+     *
+     * @since 12.0.x
+     */
+    public Date parse(String source, Date twoDigitYearStart) throws 
ParseException {
+        SimpleDateFormat sdf = queue.poll();
+        if (sdf == null) {
+            sdf = createInstance();
+        }
+        Date previousStart = sdf.get2DigitYearStart();
+        try {
+            sdf.set2DigitYearStart(twoDigitYearStart);
+            return sdf.parse(source);
+        } finally {
+            sdf.set2DigitYearStart(previousStart);
+            sdf.setTimeZone(timezone);
+            queue.add(sdf);
+        }
+    }
+
     private SimpleDateFormat createInstance() {
         SimpleDateFormat sdf = new SimpleDateFormat(format, locale);
         sdf.setTimeZone(timezone);
diff --git a/java/org/apache/tomcat/util/http/FastHttpDateFormat.java 
b/java/org/apache/tomcat/util/http/FastHttpDateFormat.java
index 3abf32b67d..9b48273b35 100644
--- a/java/org/apache/tomcat/util/http/FastHttpDateFormat.java
+++ b/java/org/apache/tomcat/util/http/FastHttpDateFormat.java
@@ -17,6 +17,8 @@
 package org.apache.tomcat.util.http;
 
 import java.text.ParseException;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
 import java.util.Date;
 import java.util.Locale;
 import java.util.Map;
@@ -147,7 +149,7 @@ public final class FastHttpDateFormat {
         long date = -1;
         for (int i = 0; (date == -1) && (i < httpParseFormats.length); i++) {
             try {
-                date = httpParseFormats[i].parse(value).getTime();
+                date = parse(httpParseFormats[i], value).getTime();
                 updateParseCache(value, Long.valueOf(date));
             } catch (ParseException e) {
                 // Ignore
@@ -158,6 +160,20 @@ public final class FastHttpDateFormat {
     }
 
 
+    private static Date parse(ConcurrentDateFormat format, String value) 
throws ParseException {
+        if (format == FORMAT_OBSOLETE_RFC850) {
+            /*
+             * RFC 9110, section 5.6.7: a two-digit year that appears to be 
more than 50 years in the future is the most
+             * recent year in the past with the same last two digits. That is 
the 100 year period that starts 50 years
+             * ago.
+             */
+            Date twoDigitYearStart = 
Date.from(ZonedDateTime.now(ZoneOffset.UTC).minusYears(50).toInstant());
+            return format.parse(value, twoDigitYearStart);
+        }
+        return format.parse(value);
+    }
+
+
     /**
      * Update cache.
      */
diff --git a/test/org/apache/tomcat/util/http/TestConcurrentDateFormat.java 
b/test/org/apache/tomcat/util/http/TestConcurrentDateFormat.java
index 51731a00fc..2830a4d984 100644
--- a/test/org/apache/tomcat/util/http/TestConcurrentDateFormat.java
+++ b/test/org/apache/tomcat/util/http/TestConcurrentDateFormat.java
@@ -45,6 +45,17 @@ public class TestConcurrentDateFormat {
         Assert.assertTrue(formattedDate.endsWith("GMT"));
     }
 
+    @Test
+    public void testParseTwoDigitYearStart() throws Exception {
+        ConcurrentDateFormat format = new ConcurrentDateFormat("dd-MMM-yy", 
Locale.US, tz);
+        Date start = format.parse("01-Jan-2000");
+
+        Assert.assertEquals(format.parse("01-Jan-2070"), 
format.parse("01-Jan-70", start));
+        // The start of the period applies to that call only
+        ConcurrentDateFormat unused = new ConcurrentDateFormat("dd-MMM-yy", 
Locale.US, tz);
+        Assert.assertEquals(unused.parse("01-Jan-70"), 
format.parse("01-Jan-70"));
+    }
+
     private ConcurrentDateFormat createConcurrentDateFormat() {
         return new ConcurrentDateFormat(DATE_RFC5322, Locale.US, tz);
     }
diff --git a/test/org/apache/tomcat/util/http/TestFastHttpDateFormat.java 
b/test/org/apache/tomcat/util/http/TestFastHttpDateFormat.java
index 20e771ef1e..4ff20545ee 100644
--- a/test/org/apache/tomcat/util/http/TestFastHttpDateFormat.java
+++ b/test/org/apache/tomcat/util/http/TestFastHttpDateFormat.java
@@ -16,11 +16,48 @@
  */
 package org.apache.tomcat.util.http;
 
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+
 import org.junit.Assert;
 import org.junit.Test;
 
 public class TestFastHttpDateFormat {
 
+    private static final DateTimeFormatter RFC850 =
+            DateTimeFormatter.ofPattern("EEEE, dd-MMM-yy HH:mm:ss 'GMT'", 
Locale.US);
+
+    /*
+     * RFC 9110, section 5.6.7: a two-digit year that appears to be more than 
50 years in the future is the most recent
+     * year in the past with the same last two digits.
+     */
+    @Test
+    public void testParseRfc850TwoDigitYear() {
+        ZonedDateTime now = 
ZonedDateTime.now(ZoneOffset.UTC).withMonth(6).withDayOfMonth(15).withHour(12)
+                .withMinute(30).withSecond(15).withNano(0);
+
+        // Not more than 50 years in the future, so the year is in the future
+        doTestParseRfc850(now.plusYears(10));
+        doTestParseRfc850(now.plusYears(40));
+        // More than 50 years in the future, so the year is in the past
+        doTestParseRfc850(now.plusYears(60), now.minusYears(40));
+        doTestParseRfc850(now.plusYears(90), now.minusYears(10));
+        // In the past
+        doTestParseRfc850(now.minusYears(10));
+        doTestParseRfc850(now.minusYears(45));
+    }
+
+    private void doTestParseRfc850(ZonedDateTime expected) {
+        doTestParseRfc850(expected, expected);
+    }
+
+    private void doTestParseRfc850(ZonedDateTime formatted, ZonedDateTime 
expected) {
+        String value = RFC850.format(formatted);
+        Assert.assertEquals(value, expected.toInstant().toEpochMilli(), 
FastHttpDateFormat.parseDate(value));
+    }
+
     @Test
     public void testGetCurrentDate() {
         /*


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to