CVE-2021-29510 (published 2021-05-13T19:15Z)

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.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:N/A:P)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-835
cwe_name: Loop with Unreachable Exit Condition ('Infinite Loop')
description: The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
extended_description: If the loop can be influenced by an attacker, this weakness could allow attackers to consume excessive resources such as CPU or memory.
         

Fix information:
repo_url: https://github.com/samuelcolvin/pydantic
commit_details: repo_url: https://github.com/samuelcolvin/pydantic
author: Samuel Colvin
author_date: 2021-05-11 18:08:48+01:00
msg: Merge pull request from GHSA-5jqp-qgf6-3pvh

* fix infinite loop in datetime parsing

* add change description

* switch to set a max datetime number
repository: repo_url: https://github.com/samuelcolvin/pydantic
repo_name: pydantic/pydantic
description: Data validation using Python type hints
repo_language: Python
owner: samuelcolvin
file_changes: filename: 2776-samuelcolvin.md
change_type: ModificationType.ADD
diff: @@ -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.

programming_language: SQL
filename: datetime_parse.py
change_type: ModificationType.MODIFY
diff: @@ -58,6 +58,8 @@ 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]
 
 
@@ -73,6 +75,11 @@ def get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[Non
 
 
 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)

programming_language: Python
filename: test_datetime_parse.py
change_type: ModificationType.MODIFY
diff: @@ -42,11 +42,20 @@ def create_tz(minutes):
         (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
@@ -123,11 +132,19 @@ def test_time_parsing(value, result):
         (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
@@ -251,3 +268,24 @@ def test_unicode_decode_error(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',
+        },
+    ]

programming_language: Python