Skip to content

Commit e81960f

Browse files
gh-152204: Validate date and time fields in _pydatetime.date{time}.fromisoformat (#152205)
Co-authored-by: Stan Ulbrych <stan@python.org>
1 parent a728080 commit e81960f

3 files changed

Lines changed: 44 additions & 11 deletions

File tree

Lib/_pydatetime.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -355,19 +355,30 @@ def _find_isoformat_datetime_separator(dtstr):
355355
return 8
356356

357357

358+
def _read_isoformat_component(s, n):
359+
# The caller has verified the string is ASCII, so isdigit() matches only
360+
# the ASCII digits accepted by the C parser.
361+
if len(s) != n or not s.isdigit():
362+
raise ValueError("Invalid isoformat string")
363+
return int(s)
364+
365+
358366
def _parse_isoformat_date(dtstr):
359367
# It is assumed that this is an ASCII-only string of lengths 7, 8 or 10,
360368
# see the comment on Modules/_datetimemodule.c:_find_isoformat_datetime_separator
361369
if len(dtstr) not in (7, 8, 10):
362370
raise ValueError("Invalid isoformat string")
363-
year = int(dtstr[0:4])
371+
if not dtstr.isascii():
372+
raise ValueError("Invalid isoformat string")
373+
374+
year = _read_isoformat_component(dtstr[0:4], 4)
364375
has_sep = dtstr[4] == '-'
365376

366377
pos = 4 + has_sep
367378
if dtstr[pos:pos + 1] == "W":
368379
# YYYY-?Www-?D?
369380
pos += 1
370-
weekno = int(dtstr[pos:pos + 2])
381+
weekno = _read_isoformat_component(dtstr[pos:pos + 2], 2)
371382
pos += 2
372383

373384
dayno = 1
@@ -377,17 +388,17 @@ def _parse_isoformat_date(dtstr):
377388

378389
pos += has_sep
379390

380-
dayno = int(dtstr[pos:pos + 1])
391+
dayno = _read_isoformat_component(dtstr[pos:pos + 1], 1)
381392

382393
return list(_isoweek_to_gregorian(year, weekno, dayno))
383394
else:
384-
month = int(dtstr[pos:pos + 2])
395+
month = _read_isoformat_component(dtstr[pos:pos + 2], 2)
385396
pos += 2
386397
if (dtstr[pos:pos + 1] == "-") != has_sep:
387398
raise ValueError("Inconsistent use of dash separator")
388399

389400
pos += has_sep
390-
day = int(dtstr[pos:pos + 2])
401+
day = _read_isoformat_component(dtstr[pos:pos + 2], 2)
391402

392403
return [year, month, day]
393404

@@ -402,10 +413,7 @@ def _parse_hh_mm_ss_ff(tstr):
402413
time_comps = [0, 0, 0, 0]
403414
pos = 0
404415
for comp in range(0, 3):
405-
if (len_str - pos) < 2:
406-
raise ValueError("Incomplete time component")
407-
408-
time_comps[comp] = int(tstr[pos:pos+2])
416+
time_comps[comp] = _read_isoformat_component(tstr[pos:pos+2], 2)
409417

410418
pos += 2
411419
next_char = tstr[pos:pos+1]
@@ -426,7 +434,7 @@ def _parse_hh_mm_ss_ff(tstr):
426434
raise ValueError("Invalid microsecond separator")
427435
else:
428436
pos += 1
429-
if not all(map(_is_ascii_digit, tstr[pos:])):
437+
if not tstr[pos:].isdigit():
430438
raise ValueError("Non-digit values in fraction")
431439

432440
len_remainder = len_str - pos
@@ -447,6 +455,8 @@ def _parse_isoformat_time(tstr):
447455
len_str = len(tstr)
448456
if len_str < 2:
449457
raise ValueError("Isoformat time too short")
458+
if not tstr.isascii():
459+
raise ValueError("Invalid isoformat string")
450460

451461
# This is equivalent to re.search('[+-Z]', tstr), but faster
452462
tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1 or tstr.find('Z') + 1)

Lib/test/datetimetester.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2106,7 +2106,15 @@ def test_fromisoformat_fails(self):
21062106
'10000-W25-1', # Invalid year
21072107
'2020-W25-0', # Invalid day-of-week
21082108
'2020-W25-8', # Invalid day-of-week
2109-
'٢025-03-09' # Unicode characters
2109+
# gh-152204: each fixed-width field must be exactly N ASCII digits
2110+
'2020+12', # '+' in a basic-format field
2111+
'2020 12', # space in a basic-format field
2112+
'+020-06-15', # leading sign in the year
2113+
'202012+9', # '+' in the day field
2114+
'2020-W 5', # space in the week number
2115+
'2020061', # 7 chars: day slice reads a 1-character tail
2116+
'2020-W2', # 1-digit week number
2117+
'٢025-03-09', # Unicode characters
21102118
'2009\ud80002\ud80028', # Separators are surrogate codepoints
21112119
]
21122120

@@ -3758,6 +3766,15 @@ def test_fromisoformat_fails_datetime(self):
37583766
'2009-04-19T12:30:45-00:90:00', # Time zone field out from range
37593767
'2009-04-19T12:30:45-00:00:90', # Time zone field out from range
37603768
'2020-2020', # Ambiguous 9-char date portion
3769+
# gh-152204: each time field must be exactly N ASCII digits
3770+
'2020-12-12T0٥:02:03', # Unicode digit in the hour
3771+
'2020-12-12T01:0٥:03', # Unicode digit in the minute
3772+
'2020-12-12T01:02:0٥', # Unicode digit in the second
3773+
'2020-12-12T01:02:03.٥', # Unicode digit in the fraction
3774+
'2020-12-12T01:02:03.4_6', # underscore in the fraction
3775+
'2020-12-12T01:02:03+0٥:00', # Unicode digit in the tz hour
3776+
'2020-12-12T01:02:03+01:0٥', # Unicode digit in the tz minute
3777+
'20201212T0102٣٤', # Unicode digits in the basic-format time
37613778
'2009-04-19T12:30:45.+05:00', # Empty fraction before offset
37623779
'2009-04-19T12:30:45.-05:00', # Empty fraction before offset
37633780
'2009-04-19T12:30:45.Z', # Empty fraction before Z
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix the pure-Python implementations of :meth:`datetime.date.fromisoformat`,
2+
:meth:`datetime.time.fromisoformat` and :meth:`datetime.datetime.fromisoformat`
3+
silently accepting some malformed ISO 8601 strings, such as non-ASCII digits or
4+
a sign in a fixed-width field (for example ``'2020+12'`` or ``'20201212T0102٣٤'``).
5+
Each field is now required to be exactly *N* ASCII digits, matching the C
6+
implementation.

0 commit comments

Comments
 (0)