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

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


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

commit b28e00d5ec4100bb5e92854c6df62f690b5e5aa0
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 ++++++++++++++++++++++
 webapps/docs/changelog.xml                         |  7 ++++
 5 files changed, 101 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() {
         /*
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 38f8c5b9a4..f9faa6c1bb 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -275,6 +275,13 @@
         Ensure the DefaultServlet returns the correct Allow header for
         <code>PATCH</code> and <code>QUERY</code> requests. (markt)
       </fix>
+      <fix>
+        Correctly parse RFC 850 formatted dates with two digit years where the
+        date appears to be more than 50 years in the future as required by RFC
+        9110. Treat the year as the most recent year in the past that had the
+        same last two digits. Based on pull request <pr>1104</pr> by Arturo
+        Bernal. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">


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

Reply via email to