https://github.com/python/cpython/commit/71c42b778dfc0831734bb7bc6121ffd44beae1d3
commit: 71c42b778dfc0831734bb7bc6121ffd44beae1d3
branch: main
author: Semyon Moroz <donbar...@proton.me>
committer: pganssle <1377457+pgans...@users.noreply.github.com>
date: 2025-05-19T14:07:11-04:00
summary:

gh-126883: Add check that timezone fields are in range for 
`datetime.fromisoformat` (#127242)

It was previously possible to specify things like `+00:90:00` which would be 
equivalent to `+01:30:00`, but is not a valid ISO8601 string.

---------

Co-authored-by: Erlend E. Aasland <erlend.aasl...@protonmail.com>
Co-authored-by: Paul Ganssle <1377457+pgans...@users.noreply.github.com>

files:
A Misc/NEWS.d/next/Library/2024-11-25-10-22-08.gh-issue-126883.MAEF7g.rst
M Lib/_pydatetime.py
M Lib/test/datetimetester.py
M Misc/ACKS
M Modules/_datetimemodule.c

diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py
index 471e89c16a1c0e..71f619024e570d 100644
--- a/Lib/_pydatetime.py
+++ b/Lib/_pydatetime.py
@@ -467,6 +467,7 @@ def _parse_isoformat_time(tstr):
     hour, minute, second, microsecond = time_comps
     became_next_day = False
     error_from_components = False
+    error_from_tz = None
     if (hour == 24):
         if all(time_comp == 0 for time_comp in time_comps[1:]):
             hour = 0
@@ -500,14 +501,22 @@ def _parse_isoformat_time(tstr):
         else:
             tzsign = -1 if tstr[tz_pos - 1] == '-' else 1
 
-            td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],
-                           seconds=tz_comps[2], microseconds=tz_comps[3])
-
-            tzi = timezone(tzsign * td)
+            try:
+                # This function is intended to validate datetimes, but because
+                # we restrict time zones to ±24h, it serves here as well.
+                _check_time_fields(hour=tz_comps[0], minute=tz_comps[1],
+                                   second=tz_comps[2], microsecond=tz_comps[3],
+                                   fold=0)
+            except ValueError as e:
+                error_from_tz = e
+            else:
+                td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],
+                               seconds=tz_comps[2], microseconds=tz_comps[3])
+                tzi = timezone(tzsign * td)
 
     time_comps.append(tzi)
 
-    return time_comps, became_next_day, error_from_components
+    return time_comps, became_next_day, error_from_components, error_from_tz
 
 # tuple[int, int, int] -> tuple[int, int, int] version of date.fromisocalendar
 def _isoweek_to_gregorian(year, week, day):
@@ -1633,9 +1642,21 @@ def fromisoformat(cls, time_string):
         time_string = time_string.removeprefix('T')
 
         try:
-            return cls(*_parse_isoformat_time(time_string)[0])
-        except Exception:
-            raise ValueError(f'Invalid isoformat string: {time_string!r}')
+            time_components, _, error_from_components, error_from_tz = (
+                _parse_isoformat_time(time_string)
+            )
+        except ValueError:
+            raise ValueError(
+                f'Invalid isoformat string: {time_string!r}') from None
+        else:
+            if error_from_tz:
+                raise error_from_tz
+            if error_from_components:
+                raise ValueError(
+                    "Minute, second, and microsecond must be 0 when hour is 24"
+                )
+
+            return cls(*time_components)
 
     def strftime(self, format):
         """Format using strftime().  The date part of the timestamp passed
@@ -1947,11 +1968,16 @@ def fromisoformat(cls, date_string):
 
         if tstr:
             try:
-                time_components, became_next_day, error_from_components = 
_parse_isoformat_time(tstr)
+                (time_components,
+                 became_next_day,
+                 error_from_components,
+                 error_from_tz) = _parse_isoformat_time(tstr)
             except ValueError:
                 raise ValueError(
                     f'Invalid isoformat string: {date_string!r}') from None
             else:
+                if error_from_tz:
+                    raise error_from_tz
                 if error_from_components:
                     raise ValueError("minute, second, and microsecond must be 
0 when hour is 24")
 
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index d1882a310bbbb0..345698cfb5f1a4 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -3571,6 +3571,10 @@ def test_fromisoformat_fails_datetime(self):
             '2009-04-19T12:30:45.400 +02:30',  # Space between ms and timezone 
(gh-130959)
             '2009-04-19T12:30:45.400 ',        # Trailing space (gh-130959)
             '2009-04-19T12:30:45. 400',        # Space before fraction 
(gh-130959)
+            '2009-04-19T12:30:45+00:90:00', # Time zone field out from range
+            '2009-04-19T12:30:45+00:00:90', # Time zone field out from range
+            '2009-04-19T12:30:45-00:90:00', # Time zone field out from range
+            '2009-04-19T12:30:45-00:00:90', # Time zone field out from range
         ]
 
         for bad_str in bad_strs:
@@ -4795,6 +4799,11 @@ def test_fromisoformat_fails(self):
             '12:30:45.400 +02:30',      # Space between ms and timezone 
(gh-130959)
             '12:30:45.400 ',            # Trailing space (gh-130959)
             '12:30:45. 400',            # Space before fraction (gh-130959)
+            '24:00:00.000001',          # Has non-zero microseconds on 24:00
+            '24:00:01.000000',          # Has non-zero seconds on 24:00
+            '24:01:00.000000',          # Has non-zero minutes on 24:00
+            '12:30:45+00:90:00',        # Time zone field out from range
+            '12:30:45+00:00:90',        # Time zone field out from range
         ]
 
         for bad_str in bad_strs:
diff --git a/Misc/ACKS b/Misc/ACKS
index 610dcf9f4238de..5653c52c9e354e 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1288,6 +1288,7 @@ Paul Moore
 Ross Moore
 Ben Morgan
 Emily Morehouse
+Semyon Moroz
 Derek Morr
 James A Morrison
 Martin Morrison
diff --git 
a/Misc/NEWS.d/next/Library/2024-11-25-10-22-08.gh-issue-126883.MAEF7g.rst 
b/Misc/NEWS.d/next/Library/2024-11-25-10-22-08.gh-issue-126883.MAEF7g.rst
new file mode 100644
index 00000000000000..5e3fa39acf1979
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-11-25-10-22-08.gh-issue-126883.MAEF7g.rst
@@ -0,0 +1,3 @@
+Add check that timezone fields are in range for
+:meth:`datetime.datetime.fromisoformat` and
+:meth:`datetime.time.fromisoformat`. Patch by Semyon Moroz.
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index 9bba0e3354b26b..313a72e3fe0668 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -1088,6 +1088,7 @@ parse_isoformat_time(const char *dtstr, size_t dtlen, int 
*hour, int *minute,
     //     -3:  Failed to parse time component
     //     -4:  Failed to parse time separator
     //     -5:  Malformed timezone string
+    //     -6:  Timezone fields are not in range
 
     const char *p = dtstr;
     const char *p_end = dtstr + dtlen;
@@ -1134,6 +1135,11 @@ parse_isoformat_time(const char *dtstr, size_t dtlen, 
int *hour, int *minute,
     rv = parse_hh_mm_ss_ff(tzinfo_pos, p_end, &tzhour, &tzminute, &tzsecond,
                            tzmicrosecond);
 
+    // Check if timezone fields are in range
+    if (check_time_args(tzhour, tzminute, tzsecond, *tzmicrosecond, 0) < 0) {
+        return -6;
+    }
+
     *tzoffset = tzsign * ((tzhour * 3600) + (tzminute * 60) + tzsecond);
     *tzmicrosecond *= tzsign;
 
@@ -5039,6 +5045,9 @@ time_fromisoformat(PyObject *cls, PyObject *tstr) {
                                   &tzoffset, &tzimicrosecond);
 
     if (rv < 0) {
+        if (rv == -6) {
+            goto error;
+        }
         goto invalid_string_error;
     }
 
@@ -5075,6 +5084,9 @@ time_fromisoformat(PyObject *cls, PyObject *tstr) {
 invalid_string_error:
     PyErr_Format(PyExc_ValueError, "Invalid isoformat string: %R", tstr);
     return NULL;
+
+error:
+    return NULL;
 }
 
 
@@ -5927,6 +5939,9 @@ datetime_fromisoformat(PyObject *cls, PyObject *dtstr)
         len -= (p - dt_ptr);
         rv = parse_isoformat_time(p, len, &hour, &minute, &second,
                                   &microsecond, &tzoffset, &tzusec);
+        if (rv == -6) {
+            goto error;
+        }
     }
     if (rv < 0) {
         goto invalid_string_error;

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to