Skip to content

Commit

Permalink
Merge pull request from GHSA-5jqp-qgf6-3pvh
Browse files Browse the repository at this point in the history
* fix infinite loop in datetime parsing

* add change description

* switch to set a max datetime number
  • Loading branch information
samuelcolvin committed May 11, 2021
1 parent 00a128a commit 80e0dd3
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 4 deletions.
2 changes: 2 additions & 0 deletions 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.
7 changes: 7 additions & 0 deletions pydantic/datetime_parse.py
Expand Up @@ -58,6 +58,8 @@
# 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]


Expand All @@ -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)
Expand Down
46 changes: 42 additions & 4 deletions tests/test_datetime_parse.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -251,3 +268,24 @@ class Model(BaseModel):
'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',
},
]

0 comments on commit 80e0dd3

Please sign in to comment.