Skip to content

Commit 3d932d8

Browse files
myteronBartKaras1128andrew-costello
authored
pySCG bugfix for CWE-191 as per #835 (#838)
* pySCG bugfix for CWE-191 as per #835 --------- Co-authored-by: BartKaras1128 <[email protected]> Signed-off-by: myteron <[email protected]> Co-authored-by: andrew-costello <[email protected]>
1 parent cf7950e commit 3d932d8

File tree

3 files changed

+222
-57
lines changed

3 files changed

+222
-57
lines changed

docs/Secure-Coding-Guide-for-Python/CWE-682/CWE-191/README.md

Lines changed: 122 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -64,61 +64,149 @@ except OverflowError as e:
6464

6565
## Non-Compliant Code Example
6666

67-
The `noncompliant02.py` example tries to use `time.localtime()` to get `x` hours in the future but causes integer overflow as the given Python `int` is too large to convert to `C long`. This is possible because `time` implements C representations of integers with all the security vulnerabilities as if you were using `C`.
67+
The `noncompliant02.py` example uses `datetime.timedelta()` to get `x` hours in the future or past for time travelers. The `datetime` is interfacing with the operating system through the `libpython` library written in `C`. Overall the Georgian calender ISO 8601 is limited to 1 - 9999 years [Python datetime 2025](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes).
6868

6969
*[noncompliant02.py](noncompliant02.py):*
7070

7171
```python
72-
""" Non-compliant Code Example """
73-
74-
import time
75-
76-
77-
def get_time_in_future(hours_in_future):
78-
"""Gets the time n hours in the future"""
79-
currtime = [tm for tm in time.localtime()]
80-
currtime[3] = currtime[3] + hours_in_future
81-
if currtime[3] + hours_in_future > 24:
82-
currtime[3] = currtime[3] - 24
83-
return time.asctime(tuple(currtime)).split(" ")[3]
84-
85-
72+
# SPDX-FileCopyrightText: OpenSSF project contributors
73+
# SPDX-License-Identifier: MIT
74+
"""Noncompliant Code Example"""
75+
76+
from datetime import datetime, timedelta
77+
78+
79+
def get_datetime(currtime: datetime, hours: int):
80+
"""
81+
Gets the time n hours in the future or past
82+
83+
Parameters:
84+
currtime (datetime): A datetime object with the starting datetime.
85+
hours (int): Hours going forward or backwards
86+
87+
Returns:
88+
datetime: A datetime object
89+
"""
90+
return currtime + timedelta(hours=hours)
91+
92+
8693
#####################
87-
# exploiting above code example
94+
# attempting to exploit above code example
8895
#####################
89-
print(get_time_in_future(23**74))
96+
datetime.fromtimestamp(0)
97+
currtime = datetime.fromtimestamp(1) # 1st Jan 1970
98+
99+
# OK values are expected to work
100+
# NOK values trigger OverflowErrors in libpython written in C
101+
hours_list = [
102+
0, # OK
103+
1, # OK
104+
70389526, # OK
105+
70389527, # NOK
106+
51539700001, # NOK
107+
24000000001, # NOK
108+
-1, # OK
109+
-17259889, # OK
110+
-17259890, # NOK
111+
-23999999999, # NOK
112+
-51539699999, # NOK
113+
]
114+
for hours in hours_list:
115+
try:
116+
result = get_datetime(currtime, hours)
117+
print(f"{hours} OK, datetime='{result}'")
118+
except Exception as exception:
119+
print(f"{hours} {repr(exception)}")
90120
```
91121

122+
The `noncompliant02.py` code is triggering various `OverflowError` exceptions in the `libpython` library:
123+
124+
- `date value out of range`
125+
- `OverflowError('Python int too large to convert to C int')`
126+
- `days=1000000000; must have magnitude <= 999999999`
127+
92128
## Compliant Solution
93129

94-
This `compliant02.py` solution handles `OverflowError` Exception when a too large value is given to `get_time_in_future`.
130+
This `compliant02.py` solution is preventing `OverflowError` exception in `libpython` by safeguarding the upper and lower limits in the provided `hours`. Upper and lower limit for `currtime` as well as input sanitization and secure logging are missing and must be added when interfacing with a lesser trusted entity.
95131

96132
*[compliant02.py](compliant02.py):*
97133

98134
```python
99-
""" Compliant Code Example """
135+
# SPDX-FileCopyrightText: OpenSSF project contributors
136+
# SPDX-License-Identifier: MIT
137+
"""Compliant Code Example"""
100138

101-
import time
139+
from datetime import datetime, timedelta
140+
import logging
102141

142+
# Enabling verbose debugging
143+
# logging.basicConfig(level=logging.DEBUG)
103144

104-
def get_time_in_future(hours_in_future):
105-
"""Gets the time n hours in the future"""
106-
try:
107-
currtime = list(time.localtime())
108-
currtime[3] = currtime[3] + hours_in_future
109-
if currtime[3] + hours_in_future > 24:
110-
currtime[3] = currtime[3] - 24
111-
return time.asctime(tuple(currtime)).split(" ")[3]
112-
except OverflowError as _:
113-
return "Number too large to set time in future " + str(hours_in_future)
145+
146+
def get_datetime(currtime: datetime, hours: int):
147+
"""
148+
Gets the time n hours in the future or past
149+
150+
Parameters:
151+
currtime (datetime): A datetime object with the starting datetime.
152+
hours (int): Hours going forward or backwards
153+
154+
Returns:
155+
datetime: A datetime object
156+
"""
157+
# TODO: input sanitation
158+
# Calculate lower boundary, hours from year 1 to currtime:
159+
timedelta_lowerbound = currtime - datetime(1, 1, 1) # 1st Jan 0001
160+
hours_min = timedelta_lowerbound.total_seconds() // 3600 * -1
161+
162+
# Calculate upper boundary, hours from year 9999 to currtime:
163+
timedelta_upperbound = datetime(9999, 12, 31) - currtime
164+
hours_max = timedelta_upperbound.total_seconds() // 3600
165+
166+
# TODO: proper secure logging
167+
logging.debug(
168+
"hours_max=%s hours_min=%s hours=%s",
169+
hours_max,
170+
hours_min,
171+
hours,
172+
)
173+
if (hours > hours_max) or (hours < hours_min):
174+
raise ValueError("hours out of range")
175+
176+
return currtime + timedelta(hours=hours)
114177

115178

116179
#####################
117180
# attempting to exploit above code example
118181
#####################
119-
print(get_time_in_future(23**74))
182+
datetime.fromtimestamp(0)
183+
currtime = datetime.fromtimestamp(1) # 1st Jan 1970
184+
185+
# OK values are expected to work
186+
# NOK values trigger OverflowErrors in libpython written in C
187+
hours_list = [
188+
0, # OK
189+
1, # OK
190+
70389526, # OK
191+
70389527, # NOK
192+
51539700001, # NOK
193+
24000000001, # NOK
194+
-1, # OK
195+
-17259889, # OK
196+
-17259890, # NOK
197+
-23999999999, # NOK
198+
-51539699999, # NOK
199+
]
200+
for hours in hours_list:
201+
try:
202+
result = get_datetime(currtime, hours)
203+
print(f"{hours} OK, datetime='{result}'")
204+
except Exception as exception:
205+
print(f"{hours} {repr(exception)}")
120206
```
121207

208+
The `compliant02.py` example is protecting the lower level c-lib from an `OverflowError` by setting boundaries for valid values in `hours`. Similar issues occure with any functionality provided through the operating system.
209+
122210
## Non-Compliant Code Example
123211

124212
The `noncompliant03.py` code example results in a `OverflowError: math range error`. This is due to `math.exp` being a `C` implementation behind the scenes for better performance. So while it returns a `Python float` it does use `C` type of variables internally for the calculation in `mathmodule.c` [[cpython 2024]](https://github.com/python/cpython/blob/main/Modules/mathmodule.c).
@@ -183,5 +271,6 @@ print(calculate_exponential_value(710))
183271

184272
|||
185273
|:---|:---|
186-
|[[Python 2024]](https://docs.python.org/3.9/library/stdtypes.html)|Format String Syntax. Available from: <https://docs.python.org/3.9/library/stdtypes.html> \[Accessed 20 June 2024]|
187-
|[[cpython 2024]](https://github.com/python/cpython/blob/main/Modules/mathmodule.c)|mathmodule.c. Available from: <https://github.com/python/cpython/blob/main/Modules/mathmodule.c)> \[Accessed 20 June 2024]|
274+
|[[Python 2024]](https://docs.python.org/3.9/library/stdtypes.html)|Format String Syntax. [online] Available from: <https://docs.python.org/3.9/library/stdtypes.html> \[Accessed 20 June 2024]|
275+
|[[cpython 2024]](https://github.com/python/cpython/blob/main/Modules/mathmodule.c)|mathmodule.c. [online] Available from: <https://github.com/python/cpython/blob/main/Modules/mathmodule.c)> \[Accessed 20 June 2024]|
276+
|[Python datetime 2025]|datetime strftime() and strptime() Format Codes [online], Available from: <https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes> [Accessed 27 March 2025]|
Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,71 @@
11
# SPDX-FileCopyrightText: OpenSSF project contributors
22
# SPDX-License-Identifier: MIT
3-
""" Compliant Code Example """
3+
"""Compliant Code Example"""
44

5-
import time
5+
from datetime import datetime, timedelta
6+
import logging
67

8+
# Enabling verbose debugging
9+
# logging.basicConfig(level=logging.DEBUG)
710

8-
def get_time_in_future(hours_in_future):
9-
"""Gets the time n hours in the future"""
10-
try:
11-
currtime = list(time.localtime())
12-
currtime[3] = currtime[3] + hours_in_future
13-
if currtime[3] + hours_in_future > 24:
14-
currtime[3] = currtime[3] - 24
15-
return time.asctime(tuple(currtime)).split(" ")[3]
16-
except OverflowError as _:
17-
return "Number too large to set time in future " + str(hours_in_future)
11+
12+
def get_datetime(currtime: datetime, hours: int):
13+
"""
14+
Gets the time n hours in the future or past
15+
16+
Parameters:
17+
currtime (datetime): A datetime object with the starting datetime.
18+
hours (int): Hours going forward or backwards
19+
20+
Returns:
21+
datetime: A datetime object
22+
"""
23+
# TODO: input sanitation
24+
# Calculate lower boundary, hours from year 1 to currtime:
25+
timedelta_lowerbound = currtime - datetime(1, 1, 1) # 1st Jan 0001
26+
hours_min = timedelta_lowerbound.total_seconds() // 3600 * -1
27+
28+
# Calculate upper boundary, hours from year 9999 to currtime:
29+
timedelta_upperbound = datetime(9999, 12, 31) - currtime
30+
hours_max = timedelta_upperbound.total_seconds() // 3600
31+
32+
# TODO: proper secure logging
33+
logging.debug(
34+
"hours_max=%s hours_min=%s hours=%s",
35+
hours_max,
36+
hours_min,
37+
hours,
38+
)
39+
if (hours > hours_max) or (hours < hours_min):
40+
raise ValueError("hours out of range")
41+
42+
return currtime + timedelta(hours=hours)
1843

1944

2045
#####################
2146
# attempting to exploit above code example
2247
#####################
23-
print(get_time_in_future(23**74))
48+
datetime.fromtimestamp(0)
49+
currtime = datetime.fromtimestamp(1) # 1st Jan 1970
50+
51+
# OK values are expected to work
52+
# NOK values trigger OverflowErrors in libpython written in C
53+
hours_list = [
54+
0, # OK
55+
1, # OK
56+
70389526, # OK
57+
70389527, # NOK
58+
51539700001, # NOK
59+
24000000001, # NOK
60+
-1, # OK
61+
-17259889, # OK
62+
-17259890, # NOK
63+
-23999999999, # NOK
64+
-51539699999, # NOK
65+
]
66+
for hours in hours_list:
67+
try:
68+
result = get_datetime(currtime, hours)
69+
print(f"{hours} OK, datetime='{result}'")
70+
except Exception as exception:
71+
print(f"{hours} {repr(exception)}")
Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,48 @@
11
# SPDX-FileCopyrightText: OpenSSF project contributors
22
# SPDX-License-Identifier: MIT
3-
""" Non-compliant Code Example """
3+
"""Noncompliant Code Example"""
44

5-
import time
5+
from datetime import datetime, timedelta
66

77

8-
def get_time_in_future(hours_in_future):
9-
"""Gets the time n hours in the future"""
10-
currtime = list(time.localtime())
11-
currtime[3] = currtime[3] + hours_in_future
12-
if currtime[3] + hours_in_future > 24:
13-
currtime[3] = currtime[3] - 24
14-
return time.asctime(tuple(currtime)).split(" ")[3]
8+
def get_datetime(currtime: datetime, hours: int):
9+
"""
10+
Gets the time n hours in the future or past
11+
12+
Parameters:
13+
currtime (datetime): A datetime object with the starting datetime.
14+
hours (int): Hours going forward or backwards
15+
16+
Returns:
17+
datetime: A datetime object
18+
"""
19+
return currtime + timedelta(hours=hours)
1520

1621

1722
#####################
18-
# exploiting above code example
23+
# attempting to exploit above code example
1924
#####################
20-
print(get_time_in_future(23**74))
25+
datetime.fromtimestamp(0)
26+
currtime = datetime.fromtimestamp(1) # 1st Jan 1970
27+
28+
# OK values are expected to work
29+
# NOK values trigger OverflowErrors in libpython written in C
30+
hours_list = [
31+
0, # OK
32+
1, # OK
33+
70389526, # OK
34+
70389527, # NOK
35+
51539700001, # NOK
36+
24000000001, # NOK
37+
-1, # OK
38+
-17259889, # OK
39+
-17259890, # NOK
40+
-23999999999, # NOK
41+
-51539699999, # NOK
42+
]
43+
for hours in hours_list:
44+
try:
45+
result = get_datetime(currtime, hours)
46+
print(f"{hours} OK, datetime='{result}'")
47+
except Exception as exception:
48+
print(f"{hours} {repr(exception)}")

0 commit comments

Comments
 (0)