diff --git a/lib/HTTP/Date.pm b/lib/HTTP/Date.pm index 29ed26e..a33a2de 100644 --- a/lib/HTTP/Date.pm +++ b/lib/HTTP/Date.pm @@ -36,7 +36,7 @@ sub str2time ($;$) { # fast exit for strictly conforming string if ( $str - =~ /^[SMTWF][a-z][a-z], (\d\d) ([JFMAJSOND][a-z][a-z]) (\d\d\d\d) (\d\d):(\d\d):(\d\d) GMT$/ + =~ /^[SMTWF][a-z][a-z], ([0-9][0-9]) ([JFMAJSOND][a-z][a-z]) ([0-9][0-9][0-9][0-9]) ([0-9][0-9]):([0-9][0-9]):([0-9][0-9]) GMT$/ ) { return eval { my $t = Time::Local::timegm( $6, $5, $4, $1, $MoY{$2} - 1, $3 ); @@ -65,7 +65,7 @@ sub str2time ($;$) { # offset already zero } - elsif ( $tz =~ /^([-+])?(\d\d?):?(\d\d)?$/ ) { + elsif ( $tz =~ /^([-+])?([0-9][0-9]?):?([0-9][0-9])?$/ ) { $offset = 3600 * $2; $offset += 60 * $3 if $3; $offset *= -1 if $1 && $1 eq '-'; @@ -98,18 +98,18 @@ sub parse_date ($) { ( ( $day, $mon, $yr, $hr, $min, $sec, $tz ) = /^ - (\d\d?) # day + ([0-9][0-9]?) # day (?:\s+|[-\/]) (\w+) # month (?:\s+|[-\/]) - (\d+) # year + ([0-9]+) # year (?: (?:\s+|:) # separator before clock - (\d\d?):(\d\d) # hour:min - (?::(\d\d))? # optional seconds + ([0-9][0-9]?):([0-9][0-9]) # hour:min + (?::([0-9][0-9]))? # optional seconds )? # optional clock \s* - ([-+]?\d{2,4}|(?![APap][Mm]\b)[A-Za-z]+)? # timezone + ([-+]?[0-9]{2,4}|(?![APap][Mm]\b)[A-Za-z]+)? # timezone \s* (?:\(\w+\)|\w{3,})? # ASCII representation of timezone. \s*$ @@ -124,13 +124,13 @@ sub parse_date ($) { = /^ (\w{1,3}) # month \s+ - (\d\d?) # day + ([0-9][0-9]?) # day \s+ - (\d\d?):(\d\d) # hour:min - (?::(\d\d))? # optional seconds + ([0-9][0-9]?):([0-9][0-9]) # hour:min + (?::([0-9][0-9]))? # optional seconds \s+ (?:([A-Za-z]+)\s+)? # optional timezone - (\d+) # year + ([0-9]+) # year \s*$ # allow trailing whitespace /x ) @@ -143,12 +143,12 @@ sub parse_date ($) { = /^ (\w{3}) # month \s+ - (\d\d?) # day + ([0-9][0-9]?) # day \s+ (?: - (\d\d\d\d) | # year - (\d{1,2}):(\d{2}) # hour:min - (?::(\d\d))? # optional seconds + ([0-9][0-9][0-9][0-9]) | # year + ([0-9]{1,2}):([0-9]{2}) # hour:min + (?::([0-9][0-9]))? # optional seconds ) \s*$ /x @@ -160,18 +160,18 @@ sub parse_date ($) { ( ( $yr, $mon, $day, $hr, $min, $sec, $tz ) = /^ - (\d{4}) # year + ([0-9]{4}) # year [-\/]? - (\d\d?) # numerical month + ([0-9][0-9]?) # numerical month [-\/]? - (\d\d?) # day + ([0-9][0-9]?) # day (?: (?:\s+|[-:Tt]) # separator before clock - (\d\d?):?(\d\d) # hour:min - (?::?(\d\d(?:\.\d*)?))? # optional seconds (and fractional) + ([0-9][0-9]?):?([0-9][0-9]) # hour:min + (?::?([0-9][0-9](?:\.[0-9]*)?))? # optional seconds (and fractional) )? # optional clock \s* - ([-+]?\d\d?:?(:?\d\d)? + ([-+]?[0-9][0-9]?:?(:?[0-9][0-9])? |Z|z)? # timezone (Z is "zero meridian", i.e. GMT) \s*$ /x @@ -183,13 +183,13 @@ sub parse_date ($) { ( ( $mon, $day, $yr, $hr, $min, $ampm ) = /^ - (\d{2}) # numerical month + ([0-9]{2}) # numerical month - - (\d{2}) # day + ([0-9]{2}) # day - - (\d{2,4}) # year + ([0-9]{2,4}) # year \s+ - (\d\d?):(\d\d)([APap][Mm]) # hour:min AM or PM + ([0-9][0-9]?):([0-9][0-9])([APap][Mm]) # hour:min AM or PM \s*$ /x ) @@ -200,7 +200,7 @@ sub parse_date ($) { $mon = $MoY{$mon} || $MoY{"\u\L$mon"} - || ( $mon =~ /^\d\d?$/ && $mon >= 1 && $mon <= 12 && int($mon) ) + || ( $mon =~ /^[0-9][0-9]?$/ && $mon >= 1 && $mon <= 12 && int($mon) ) || return; # If the year is missing, we assume first date before the current, diff --git a/t/unicode-digits.t b/t/unicode-digits.t new file mode 100644 index 0000000..d61eb68 --- /dev/null +++ b/t/unicode-digits.t @@ -0,0 +1,46 @@ +#!perl + +use strict; +use warnings; + +use Test::More; +use HTTP::Date qw(str2time parse_date); + +# Regression test: date parsing must accept ASCII digits only. +# +# Perl's \d matches any Unicode digit, so a date written with non-ASCII +# digits (e.g. Arabic-Indic U+0660..U+0669) used to match the parsing +# regexes. Those Unicode digits then coerced to 0 numerically, so the +# string was "parsed" into a bogus date instead of being rejected. +# +# After hardening the regexes to [0-9], such strings must not parse. + +# Map ASCII 0-9 to the corresponding Arabic-Indic digit U+0660..U+0669. +sub to_unicode_digits { + my ($str) = @_; + $str =~ s/([0-9])/chr( 0x0660 + $1 )/ge; + return $str; +} + +my @ascii_dates = ( + 'Thu, 03 Feb 1994 00:00:00 GMT', # fast-path RFC 1123 format + '03/Feb/1994:00:00:00 0000', # common logfile format + '03-Feb-1994 00:00:00 GMT', # rfc850 (no weekday) + '1994-02-03 00:00:00', # ISO 8601 +); + +for my $ascii (@ascii_dates) { + my $unicode = to_unicode_digits($ascii); + + # Sanity: the ASCII form still parses, the Unicode form differs from it. + ok( defined str2time($ascii), "ASCII date still parses: $ascii" ); + isnt( $unicode, $ascii, 'Unicode-digit string differs from ASCII' ); + + is( str2time($unicode), undef, + 'str2time rejects Unicode-digit date (not parsed as 0)' ); + + is( scalar parse_date($unicode), undef, + 'parse_date rejects Unicode-digit date' ); +} + +done_testing;