CVE-2021-29510: Loop with Unreachable Exit Condition ('Infinite Loop') (CWE-835)
value: Pydantic is a data validation and settings management using Python type hinting. In affected versions passing either `'infinity'`, `'inf'` or `float('inf')` (or their negatives) to `datetime` or `date` fields causes validation to run forever with 100% CPU usage (on one CPU). Pydantic has been patched with fixes available in the following versions: v1.8.2, v1.7.4, v1.6.2. All these versions are available on pypi(https://pypi.org/project/pydantic/#history), and will be available on conda-forge(https://anaconda.org/conda-forge/pydantic) soon. See the changelog(https://pydantic-docs.helpmanual.io/) for details. If you absolutely can't upgrade, you can work around this risk using a validator(https://pydantic-docs.helpmanual.io/usage/validators/) to catch these values. This is not an ideal solution (in particular you'll need a slightly different function for datetimes), instead of a hack like this you should upgrade pydantic. If you are not using v1.8.x, v1.7.x or v1.6.x and are unable to upgrade to a fixed version of pydantic, please create an issue at https://github.com/samuelcolvin/pydantic/issues requesting a back-port, and we will endeavour to release a patch for earlier versions of pydantic.
CWE description: The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
CVSS v3 base score: 7.5
Published: 2021-05-13T19:15Z
Repository: https://github.com/samuelcolvin/pydantic
Language: Python
Files changed: changes/2776-samuelcolvin.md, pydantic/datetime_parse.py, tests/test_datetime_parse.py
Commit date: 2021-05-11 18:08:48 +0100
Commit message: Merge pull request from GHSA-5jqp-qgf6-3pvh

* fix infinite loop in datetime parsing

* add change description

* switch to set a max datetime number
Diff: diff --git a/changes/2776-samuelcolvin.md b/changes/2776-samuelcolvin.md
new file mode 100644
index 0000000..e190e82
--- /dev/null
+++ b/changes/2776-samuelcolvin.md
@@ -0,0 +1,2 @@
+**Security fix:** Fix `date` and `datetime` parsing so passing either `'infinity'` or `float('inf')` 
+(or their negative values) does not cause an infinite loop.
diff --git a/pydantic/datetime_parse.py b/pydantic/datetime_parse.py
index d567c5c..59466c1 100644
--- a/pydantic/datetime_parse.py
+++ b/pydantic/datetime_parse.py
@@ -56,10 +56,12 @@ iso8601_duration_re = re.compile(
 
 EPOCH = datetime(1970, 1, 1)
 # if greater than this, the number is in ms, if less than or equal it's in seconds
 # (in seconds this is 11th October 2603, in ms it's 20th August 1970)
 MS_WATERSHED = int(2e10)
+# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
+MAX_NUMBER = int(3e20)
 StrBytesIntFloat = Union[str, bytes, int, float]
 
 
 def get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]:
     if isinstance(value, (int, float)):
@@ -71,10 +73,15 @@ def get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[Non
     except TypeError:
         raise TypeError(f'invalid type; expected {native_expected_type}, string, bytes, int or float')
 
 
 def from_unix_seconds(seconds: Union[int, float]) -> datetime:
+    if seconds > MAX_NUMBER:
+        return datetime.max
+    elif seconds < -MAX_NUMBER:
+        return datetime.min
+
     while abs(seconds) > MS_WATERSHED:
         seconds /= 1000
     dt = EPOCH + timedelta(seconds=seconds)
     return dt.replace(tzinfo=timezone.utc)
 
diff --git a/tests/test_datetime_parse.py b/tests/test_datetime_parse.py
index d629d9f..f714d66 100644
--- a/tests/test_datetime_parse.py
+++ b/tests/test_datetime_parse.py
@@ -40,15 +40,24 @@ def create_tz(minutes):
         (20_000_000_001, date(1970, 8, 20)),  # just after watershed
         (1_549_316_052, date(2019, 2, 4)),  # nowish in s
         (1_549_316_052_104, date(2019, 2, 4)),  # nowish in ms
         (1_549_316_052_104_324, date(2019, 2, 4)),  # nowish in μs
         (1_549_316_052_104_324_096, date(2019, 2, 4)),  # nowish in ns
+        ('infinity', date(9999, 12, 31)),
+        ('inf', date(9999, 12, 31)),
+        (float('inf'), date(9999, 12, 31)),
+        ('infinity ', date(9999, 12, 31)),
+        (int('1' + '0' * 100), date(9999, 12, 31)),
+        (1e1000, date(9999, 12, 31)),
+        ('-infinity', date(1, 1, 1)),
+        ('-inf', date(1, 1, 1)),
+        ('nan', ValueError),
     ],
 )
 def test_date_parsing(value, result):
-    if result == errors.DateError:
-        with pytest.raises(errors.DateError):
+    if type(result) == type and issubclass(result, Exception):
+        with pytest.raises(result):
             parse_date(value)
     else:
         assert parse_date(value) == result
 
 
@@ -121,15 +130,23 @@ def test_time_parsing(value, result):
         (20_000_000_001, datetime(1970, 8, 20, 11, 33, 20, 1000, tzinfo=timezone.utc)),  # just after watershed
         (1_549_316_052, datetime(2019, 2, 4, 21, 34, 12, 0, tzinfo=timezone.utc)),  # nowish in s
         (1_549_316_052_104, datetime(2019, 2, 4, 21, 34, 12, 104_000, tzinfo=timezone.utc)),  # nowish in ms
         (1_549_316_052_104_324, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)),  # nowish in μs
         (1_549_316_052_104_324_096, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)),  # nowish in ns
+        ('infinity', datetime(9999, 12, 31, 23, 59, 59, 999999)),
+        ('inf', datetime(9999, 12, 31, 23, 59, 59, 999999)),
+        ('inf ', datetime(9999, 12, 31, 23, 59, 59, 999999)),
+        (1e50, datetime(9999, 12, 31, 23, 59, 59, 999999)),
+        (float('inf'), datetime(9999, 12, 31, 23, 59, 59, 999999)),
+        ('-infinity', datetime(1, 1, 1, 0, 0)),
+        ('-inf', datetime(1, 1, 1, 0, 0)),
+        ('nan', ValueError),
     ],
 )
 def test_datetime_parsing(value, result):
-    if result == errors.DateTimeError:
-        with pytest.raises(errors.DateTimeError):
+    if type(result) == type and issubclass(result, Exception):
+        with pytest.raises(result):
             parse_datetime(value)
     else:
         assert parse_datetime(value) == result
 
 
@@ -249,5 +266,26 @@ def test_unicode_decode_error(field):
     assert error == {
         'loc': (field,),
         'type': 'value_error.unicodedecode',
         'msg': "'utf-8' codec can't decode byte 0x81 in position 0: invalid start byte",
     }
+
+
+def test_nan():
+    class Model(BaseModel):
+        dt: datetime
+        d: date
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(dt='nan', d='nan')
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('dt',),
+            'msg': 'cannot convert float NaN to integer',
+            'type': 'value_error',
+        },
+        {
+            'loc': ('d',),
+            'msg': 'cannot convert float NaN to integer',
+            'type': 'value_error',
+        },
+    ]

Vulnerable code:     if result == errors.DateError:
        with pytest.raises(errors.DateError):
    if result == errors.DateTimeError:
        with pytest.raises(errors.DateTimeError):
Fixed code: **Security fix:** Fix `date` and `datetime` parsing so passing either `'infinity'` or `float('inf')` 
(or their negative values) does not cause an infinite loop.
# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
MAX_NUMBER = int(3e20)
    if seconds > MAX_NUMBER:
        return datetime.max
    elif seconds < -MAX_NUMBER:
        return datetime.min

        ('infinity', date(9999, 12, 31)),
        ('inf', date(9999, 12, 31)),
        (float('inf'), date(9999, 12, 31)),
        ('infinity ', date(9999, 12, 31)),
        (int('1' + '0' * 100), date(9999, 12, 31)),
        (1e1000, date(9999, 12, 31)),
        ('-infinity', date(1, 1, 1)),
        ('-inf', date(1, 1, 1)),
        ('nan', ValueError),
    if type(result) == type and issubclass(result, Exception):
        with pytest.raises(result):
        ('infinity', datetime(9999, 12, 31, 23, 59, 59, 999999)),
        ('inf', datetime(9999, 12, 31, 23, 59, 59, 999999)),
        ('inf ', datetime(9999, 12, 31, 23, 59, 59, 999999)),
        (1e50, datetime(9999, 12, 31, 23, 59, 59, 999999)),
        (float('inf'), datetime(9999, 12, 31, 23, 59, 59, 999999)),
        ('-infinity', datetime(1, 1, 1, 0, 0)),
        ('-inf', datetime(1, 1, 1, 0, 0)),
        ('nan', ValueError),
    if type(result) == type and issubclass(result, Exception):
        with pytest.raises(result):


def test_nan():
    class Model(BaseModel):
        dt: datetime
        d: date

    with pytest.raises(ValidationError) as exc_info:
        Model(dt='nan', d='nan')
    assert exc_info.value.errors() == [
        {
            'loc': ('dt',),
            'msg': 'cannot convert float NaN to integer',
            'type': 'value_error',
        },
        {
            'loc': ('d',),
            'msg': 'cannot convert float NaN to integer',
            'type': 'value_error',
        },
    ]