Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 26 additions & 26 deletions lib/HTTP/Date.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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 '-';
Expand Down Expand Up @@ -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*$
Expand All @@ -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
)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
)
Expand All @@ -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,
Expand Down
46 changes: 46 additions & 0 deletions t/unicode-digits.t
Original file line number Diff line number Diff line change
@@ -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;
Loading