From b3932113388186065e06a0de7da324112c665f8f Mon Sep 17 00:00:00 2001 From: Robert Rothenberg Date: Thu, 20 Aug 2026 19:24:17 +0100 Subject: [PATCH] fix: restrict numeric date fields to ASCII digits CVE-2026-60074 The parse regexes capture year, month and day with the \d shorthand, which on a character string matches the whole Unicode decimal digit property \p{Nd} and not just [0-9]. Date::Manip::Base::check validates the captured fields with numeric comparisons alone, and _parse_check stores the numified fields, so a field whose leading characters are ASCII digits numifies to an in-range prefix and satisfies every test. With U+0664 ARABIC-INDIC DIGIT FOUR as the final character of the year, ParseDate("202\x{664}-03-08") returns 0202030800:00:00; a non-ASCII digit in the month or day field shifts those fields the same way ("2026-1\x{662}-08" returns January, "2026-03-1\x{665}" returns the 1st), and one in a fractional minute field drops the fraction. --- lib/Date/Manip/Base.pm | 11 +++++- lib/Date/Manip/Date.pm | 84 +++++++++++++++++++++--------------------- 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/lib/Date/Manip/Base.pm b/lib/Date/Manip/Base.pm index c8dbf0dd1..1511f521b 100644 --- a/lib/Date/Manip/Base.pm +++ b/lib/Date/Manip/Base.pm @@ -603,6 +603,15 @@ sub check { my($self,$date) = @_; my($y,$m,$d,$h,$mn,$s) = @$date; + # The range tests below numify each field, and numifying truncates a + # string at the first character which is not an ASCII digit, so a + # field such as "202\x{664}" would pass them as 202. Require ASCII + # digits. + + foreach my $val ($y,$m,$d) { + return 0 if (! defined($val) || $val !~ /^[0-9]+$/); + } + return 0 if (! $self->check_time([$h,$mn,$s]) || $y<1 || $y>9999 || $m<1 || $m>12); @@ -617,7 +626,7 @@ sub check_time { my($self,$hms) = @_; my($h,$mn,$s) = @$hms; - return 0 if ("$h:$mn:$s" !~ /^\d\d?:\d\d?:\d\d?$/o || + return 0 if ("$h:$mn:$s" !~ /^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}$/o || $h > 24 || $mn > 59 || $s > 59 || ($h == 24 && ($mn || $s))); return 1; diff --git a/lib/Date/Manip/Date.pm b/lib/Date/Manip/Date.pm index bcc762c9f..60476540d 100644 --- a/lib/Date/Manip/Date.pm +++ b/lib/Date/Manip/Date.pm @@ -837,16 +837,16 @@ BEGIN { ### if ($f eq 'Y') { - $re .= '(?\d\d\d\d)'; + $re .= '(?[0-9]{4})'; } elsif ($f eq 'y') { - $re .= '(?\d\d)'; + $re .= '(?[0-9]{2})'; } elsif ($f eq 'm') { - $re .= '(?\d\d)'; + $re .= '(?[0-9]{2})'; } elsif ($f eq 'f') { - $re .= '(?:(?\d\d)| ?(?\d))'; + $re .= '(?:(?[0-9]{2})| ?(?[0-9]))'; } elsif (exists $mon_form{$f}) { my $abb = $$dmb{'data'}{'rx'}{'month_abb'}[0]; @@ -854,13 +854,13 @@ BEGIN { $re .= "(?:(?$nam)|(?$abb))"; } elsif ($f eq 'j') { - $re .= '(?\d\d\d)'; + $re .= '(?[0-9]{3})'; } elsif ($f eq 'd') { - $re .= '(?\d\d)'; + $re .= '(?[0-9]{2})'; } elsif ($f eq 'e') { - $re .= '(?:(?\d\d)| ?(?\d))'; + $re .= '(?:(?[0-9]{2})| ?(?[0-9]))'; } elsif (exists $day_form{$f}) { my $abb = $$dmb{'data'}{'rx'}{'day_abb'}[0]; @@ -876,41 +876,41 @@ BEGIN { $re .= "(?$nth)" } elsif ($f eq 'H' || $f eq 'I') { - $re .= '(?\d\d)'; + $re .= '(?[0-9]{2})'; } elsif ($f eq 'k' || $f eq 'i') { - $re .= '(?:(?\d\d)| ?(?\d))'; + $re .= '(?:(?[0-9]{2})| ?(?[0-9]))'; } elsif ($f eq 'p') { my $ampm = $$dmb{data}{rx}{ampm}[0]; $re .= "(?$ampm)"; } elsif ($f eq 'M') { - $re .= '(?\d\d)'; + $re .= '(?[0-9]{2})'; } elsif ($f eq 'S') { - $re .= '(?\d\d)'; + $re .= '(?[0-9]{2})'; } elsif (exists $z_form{$f}) { $re .= $dmt->_zrx('zrx'); } elsif ($f eq 's') { - $re .= '(?\d+)'; + $re .= '(?[0-9]+)'; } elsif ($f eq 'o') { - $re .= '(?\d+)'; + $re .= '(?[0-9]+)'; } elsif ($f eq 'G') { - $re .= '(?\d\d\d\d)'; + $re .= '(?[0-9]{4})'; } elsif ($f eq 'W') { - $re .= '(?\d\d)'; + $re .= '(?[0-9]{2})'; } elsif ($f eq 'L') { - $re .= '(?\d\d\d\d)'; + $re .= '(?[0-9]{4})'; } elsif ($f eq 'U') { - $re .= '(?\d\d)'; + $re .= '(?[0-9]{2})'; } elsif ($f eq 'c') { $format = '%a %b %e %H:%M:%S %Y' . $format; @@ -1168,15 +1168,15 @@ sub _iso8601_rx { if ($rx eq 'cdate' || $rx eq 'tdate') { - my $y4 = '(?\d\d\d\d)'; - my $y2 = '(?\d\d)'; + my $y4 = '(?[0-9]{4})'; + my $y2 = '(?[0-9]{2})'; my $m = '(?0[1-9]|1[0-2])'; my $d = '(?0[1-9]|[12][0-9]|3[01])'; my $doy = '(?00[1-9]|0[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|36[0-6])'; my $w = '(?0[1-9]|[1-4][0-9]|5[0-3])'; my $dow = '(?[1-7])'; - my $yod = '(?\d)'; - my $cc = '(?\d\d)'; + my $yod = '(?[0-9])'; + my $cc = '(?[0-9]{2})'; my @cdaterx = ( @@ -1238,9 +1238,9 @@ sub _iso8601_rx { my $h24b = '(?24(?:00){0,2})'; my $h = '(?[0-9])'; - my $fh = '(?:[\.,](?\d*))'; # fractional hours (keep) - my $fm = '(?:[\.,](?\d*))'; # fractional seconds (keep) - my $fs = '(?:[\.,]\d*)'; # fractional hours (discard) + my $fh = '(?:[\.,](?[0-9]*))'; # fractional hours (keep) + my $fm = '(?:[\.,](?[0-9]*))'; # fractional seconds (keep) + my $fs = '(?:[\.,][0-9]*)'; # fractional hours (discard) my $zrx = $dmt->_zrx('zrx'); @@ -1445,9 +1445,9 @@ sub _other_rx { $f1 = "[.,]"; $f2 = "[.,:]"; } - my $fh = "(?:$f1(?\\d*))"; # fractional hours (keep) - my $fm = "(?:$f1(?\\d*))"; # fractional minutes (keep) - my $fs = "(?:$f2\\d*)"; # fractional seconds + my $fh = "(?:$f1(?[0-9]*))"; # fractional hours (keep) + my $fm = "(?:$f1(?[0-9]*))"; # fractional minutes (keep) + my $fs = "(?:${f2}[0-9]*)"; # fractional seconds # AM/PM @@ -1533,10 +1533,10 @@ sub _other_rx { # Do NOT replace and with a regular expression to # match 1-12 since the DateFormat config may reverse the two. - my $y4 = '(?\d\d\d\d)'; - my $y2 = '(?\d\d)'; - my $m = '(?\d\d?)'; - my $d = '(?\d\d?)'; + my $y4 = '(?[0-9]{4})'; + my $y2 = '(?[0-9]{2})'; + my $m = '(?[0-9]{1,2})'; + my $d = '(?[0-9]{1,2})'; my $sep = '(?[\s\.\/\-])'; my @daterx = @@ -1555,11 +1555,11 @@ sub _other_rx { my $abb = $$dmb{'data'}{'rx'}{'month_abb'}[0]; my $nam = $$dmb{'data'}{'rx'}{'month_name'}[0]; - my $y4 = '(?\d\d\d\d)'; - my $y2 = '(?\d\d)'; - my $m = '(?\d\d?)'; - my $d = '(?\d\d?)'; - my $dd = '(?\d\d)'; + my $y4 = '(?[0-9]{4})'; + my $y2 = '(?[0-9]{2})'; + my $m = '(?[0-9]{1,2})'; + my $d = '(?[0-9]{1,2})'; + my $dd = '(?[0-9]{2})'; my $mmm = "(?:(?$abb)|(?$nam))"; my $sep = '(?[\s\.\/\-])'; @@ -1610,7 +1610,7 @@ sub _other_rx { my $abb = $$dmb{'data'}{'rx'}{'month_abb'}[0]; my $nam = $$dmb{'data'}{'rx'}{'month_name'}[0]; - my $y4 = '(?\d\d\d\d)'; + my $y4 = '(?[0-9]{4})'; my $mmm = "(?:(?$abb)|(?$nam))"; my $sep = '(?[\s\.\/\-])'; @@ -1656,11 +1656,11 @@ sub _other_rx { my $special = $$dmb{'data'}{'rx'}{'offset_time'}[0]; $special = "(?$special)"; - my $secs = "(?[-+]?\\d+)"; + my $secs = "(?[-+]?[0-9]+)"; my $abb = $$dmb{'data'}{'rx'}{'month_abb'}[0]; my $mmm = "(?$abb)"; - my $y4 = '(?\d\d\d\d)'; - my $dd = '(?\d\d)'; + my $y4 = '(?[0-9]{4})'; + my $dd = '(?[0-9]{2})'; my $h24 = '(?2[0-3]|[01][0-9])'; # 00-23 my $mn = '(?[0-5][0-9])'; # 00-59 my $ss = '(?[0-5][0-9])'; # 00-59 @@ -1697,7 +1697,7 @@ sub _other_rx { my $nth_wom = $$dmb{'data'}{'rx'}{'nth_wom'}[0]; my $special = $$dmb{'data'}{'rx'}{'offset_date'}[0]; - my $y = '(?:(?\d\d\d\d)|(?\d\d))'; + my $y = '(?:(?[0-9]{4})|(?[0-9]{2}))'; my $mmm = "(?:(?$abb)|(?$nam))"; $next = "(?$next)"; $last = "(?$last)"; @@ -1732,7 +1732,7 @@ sub _other_rx { # nth day in MMM [YYYY] "${nth}\\s+${wf}\\s*$y?", # DoW Nth week [YYYY] - "${wf}\\s+(?\\d+)\\s*$y?", # DoW week N [YYYY] + "${wf}\\s+(?[0-9]+)\\s*$y?", # DoW week N [YYYY] "${special}", # today, tomorrow "${special}\\s+${wf}", # today week