Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions docs/Secure-Coding-Guide-for-Python/CWE-703/CWE-754/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# CWE-754: Improper Check for Unusual or Exceptional Conditions - Float

Ensure to have handling for exceptional floating-point values.

The `float` class has the capability to interpret various input values as floating-point numbers. Some special cases can interpret input values as

* Positive Infinity
* Negative Infinity
* `NaN` (Not-a-Number)

These floating-point class values represent numbers that fall outside the typical range and exhibit unique behaviors. `NaN` (Not a Number) lacks a defined order and is not considered equal to any value, including itself. Hence, evaluating an expression such as `NaN == NaN` returns `False`.

## Non-Compliant Code Example

The `noncompliant01.py` intent is to ensure that adding objects will not exceed a total weight of `100` units. Validation fails as the code to test for exceptional conditions, such as `NaN` or `infinity`, is missing.

*[noncompliant01.py](noncompliant01.py):*

```py
# SPDX-FileCopyrightText: OpenSSF project contributors
# SPDX-License-Identifier: MIT
"""Non-compliant Code Example"""

import sys


class Package:
"""Class representing a package object"""

def __init__(self):
self.package_weight: float = 0.0
self.max_package_weight: float = 100.0

def add_to_package(self, object_weight: str):
"""Function for adding an object into the package"""
value = float(object_weight)
# This is dead code as value gets type cast to float,
# hence will never be equal to string "NaN"
if value == "NaN":
raise ValueError("'NaN' not a number")
# This is also dead code as value is type cast to float,
# unusual inputs like -infinity will not get caught
if isinstance(value, float) is False:
raise ValueError("not a number")
if self.package_weight + value > self.max_package_weight:
raise ValueError("Addition would exceed maximum package weight.")

self.package_weight += value

def get_package_weight(self):
"""Getter for outputting the package's current weight"""
return self.package_weight


#####################
# exploiting above code example
#####################
package = Package()
print(f"\nOriginal package's weight is {package.get_package_weight():.2f} units\n")
for item in [100, "-infinity", sys.float_info.max, "NaN", -100]:
print(f"package.add_to_package({item})")
try:
package.add_to_package(item)
print(
f"package.get_package_weight() = {package.get_package_weight():.2f}\n"
)

except Exception as e:
print(e)
```

Some important considerations when dealing with floating-point values from `non-complaint01.py`.

* Setting a value above `sys.float_info.max` does not increase the held value. In some cases, incrementing `package_weight` with a high enough value may turn its value into `inf`.
* Setting the added value to `-infinity` and `+infinity` causes the value of the `package_weight` to be infinite as well.
* Adding `"NaN"`, which is not a valid value to `package_weight` will always return `"nan"`.

**Example `noncompliant01.py` output:**

```bash
Original package's weight is 0.00 units

package.add_to_package(100)
package.get_package_weight() = 100.00

package.add_to_package(-infinity)
package.get_package_weight() = -inf

package.add_to_package(1.7976931348623157e+308)
package.get_package_weight() = -inf

package.add_to_package(NaN)
package.get_package_weight() = nan

package.add_to_package(-100)
package.get_package_weight() = nan
```

## Compliant Solution

Exceptional values and out-of-range values are handled in `compliant01.py`. Some negative values are also checked for due to the nature of the code example.
The `isfinite` function from the `math` library is useful for checking for `NaN`, `infinity` and `-infinity` values. `math.isfinite` checks if a value is neither `infinite` nor a `NaN`.

Other functions from the `math` library that could be of use are `isnan`, which checks if an inputted value is `"NaN"`, and `isinf` (which checks if a value is positive or negative infinity).

*[compliant01.py](compliant01.py):*

```py
# SPDX-FileCopyrightText: OpenSSF project contributors
# SPDX-License-Identifier: MIT
""" Compliant Code Example """


import sys
from math import isfinite, isnan
from typing import Union


class Package:
"""Class representing a package object."""
def __init__(self) -> None:
self.package_weight: float = 0.0
self.max_package_weight: float = 100.0

def add_to_package(self, object_weight: Union[str, int, float]) -> None:
# TODO: input sanitation.
# TODO: proper exception handling
"""Add an object into the package after validating its weight."""
try:
value = float(object_weight)
except (ValueError, TypeError) as e:
raise ValueError("Input cannot be converted to a float.") from e
if isnan(value):
raise ValueError("Input is not a number")
if not isfinite(value):
raise ValueError("Input is not a finite number.")
if value < 0:
raise ValueError("Weight must be a non-negative number.")
if self.package_weight + value > self.max_package_weight:
raise ValueError("Addition would exceed maximum package weight.")

print(f"Adding an object that weighs {value} units to package")
self.package_weight += value

def get_package_weight(self) -> float:
"""Return the package's current weight."""
return self.package_weight


#####################
# exploiting above code example
#####################


package = Package()
print(f"\nOriginal package's weight is {package.get_package_weight():.2f} units\n")
for item in [100, "-infinity", sys.float_info.max, "NaN", -100]:
print(f"package.add_to_package({item})")
try:
package.add_to_package(item)
print(
f"package.get_package_weight() = {package.get_package_weight():.2f}\n"
)

except Exception as e:
print(e)
```

This compliant code example will raise a `ValueError` for inputs that are `-infinity`, `infinity`, or `NaN`, with messages "Input is not a finite number" and "Input is not a number" respectively. It should also ensure weights are non-negative, returning "Weight must be a non-negative number" for negative inputs.

**Example `compliant01.py` output:**

```bash
Original package's weight is 0.00 units

package.add_to_package(100)
Adding an object that weighs 100.0 units to package
package.get_package_weight() = 100.00

package.add_to_package(-infinity)
Input is not a finite number.
package.add_to_package(1.7976931348623157e+308)
Addition would exceed maximum package weight.
package.add_to_package(NaN)
Input is not a number
package.add_to_package(-100)
Weight must be a non-negative number.
```

## Automated Detection

|||||
|:---|:---|:---|:---|
|Tool|Version|Checker|Description|
|bandit|1.7.4|no detection||

## Related Guidelines

|||
|:---|:---|
|[SEI CERT C Coding Standard](https://wiki.sei.cmu.edu/confluence/display/c/SEI+CERT+C+Coding+Standard)|[FLP04-C. Check floating-point inputs for exceptional values](https://wiki.sei.cmu.edu/confluence/display/c/FLP04-C.+Check+floating-point+inputs+for+exceptional+values)|
|[SEI CERT Oracle Coding Standard for Java](https://wiki.sei.cmu.edu/confluence/display/java/SEI+CERT+Oracle+Coding+Standard+for+Java?src=sidebar)|[NUM08-J. Check floating-point inputs for exceptional values](https://wiki.sei.cmu.edu/confluence/display/java/NUM08-J.+Check+floating-point+inputs+for+exceptional+values)|
|[CWE MITRE Pillar](http://cwe.mitre.org/)|[CWE-703: Improper Check or Handling of Exceptional Conditions](https://cwe.mitre.org/data/definitions/703.html)|
|[MITRE CWE Base](https://cwe.mitre.org/)|[CWE-754: Improper Check for Unusual or Exceptional Conditions](https://cwe.mitre.org/data/definitions/754.html)|
58 changes: 35 additions & 23 deletions docs/Secure-Coding-Guide-for-Python/CWE-703/CWE-754/compliant01.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,58 @@
# SPDX-FileCopyrightText: OpenSSF project contributors
# SPDX-License-Identifier: MIT
""" Compliant Code Example """


import sys
from math import isinf, isnan
from math import isfinite, isnan
from typing import Union


class Package:
def __init__(self):
self.package_weight = float(1.0)

def put_in_the_package(self, user_input):
"""Class representing a package object."""
def __init__(self) -> None:
self.package_weight: float = 0.0
self.max_package_weight: float = 100.0

def add_to_package(self, object_weight: Union[str, int, float]) -> None:
"""Add an object into the package after validating its weight."""
# TODO: input sanitation.
# TODO: proper exception handling
try:
value = float(user_input)
except ValueError:
raise ValueError(f"{user_input} - Input is not a number!")

print(f"value is {value}")

if isnan(value) or isinf(value):
raise ValueError(f"{user_input} - Input is not a real number!")

value = float(object_weight)
except (ValueError, TypeError) as e:
raise ValueError("Input cannot be converted to a float.") from e
if isnan(value):
raise ValueError("Input is not a number")
if not isfinite(value):
raise ValueError("Input is not a finite number.")
if value < 0:
raise ValueError(
f"{user_input} - Packed object weight cannot be negative!"
)
raise ValueError("Weight must be a non-negative number.")
if self.package_weight + value > self.max_package_weight:
raise ValueError("Addition would exceed maximum package weight.")

if value >= sys.float_info.max - self.package_weight:
raise ValueError(f"{user_input} - Input exceeds acceptable range!")
print(f"Adding an object that weighs {value} units to package")
self.package_weight += value

def get_package_weight(self):
def get_package_weight(self) -> float:
"""Return the package's current weight."""
return self.package_weight


#####################
# exploiting above code example
#####################


package = Package()
print(f"\nOriginal package's weight is {package.get_package_weight():.2f} units\n")
for item in [sys.float_info.max, "infinity", "-infinity", "nan"]:
for item in [100, "-infinity", sys.float_info.max, "NaN", -100]:
print(f"package.add_to_package({item})")
try:
package.put_in_the_package(item)
print(f"Current package weight = {package.get_package_weight():.2f}\n")
package.add_to_package(item)
print(
f"package.get_package_weight() = {package.get_package_weight():.2f}\n"
)

except Exception as e:
print(e)
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
# SPDX-FileCopyrightText: OpenSSF project contributors
# SPDX-License-Identifier: MIT
""" Non-compliant Code Example """
"""Non-compliant Code Example"""

import sys


class Package:
"""Class representing a package object"""

def __init__(self):
self.package_weight = float(1.0)
self.package_weight: float = 0.0
self.max_package_weight: float = 100.0

def put_in_the_package(self, object_weight):
def add_to_package(self, object_weight: str):
"""Function for adding an object into the package"""
value = float(object_weight)
print(f"Adding an object that weighs {value} units")
# This is dead code as value gets type cast to float,
# hence will never be equal to string "NaN"
if value == "NaN":
raise ValueError("'NaN' not a number")
# This is also dead code as value is type cast to float,
# unusual inputs like -infinity will not get caught
if isinstance(value, float) is False:
raise ValueError("not a number")
if self.package_weight + value > self.max_package_weight:
raise ValueError("Addition would exceed maximum package weight.")

self.package_weight += value

def get_package_weight(self):
"""Getter for outputting the package's current weight"""
return self.package_weight


Expand All @@ -22,9 +38,13 @@ def get_package_weight(self):
#####################
package = Package()
print(f"\nOriginal package's weight is {package.get_package_weight():.2f} units\n")
for item in [sys.float_info.max, "infinity", "-infinity", "nan"]:
for item in [100, "-infinity", sys.float_info.max, "NaN", -100]:
print(f"package.add_to_package({item})")
try:
package.put_in_the_package(item)
print(f"Current package weight = {package.get_package_weight():.2f}\n")
package.add_to_package(item)
print(
f"package.get_package_weight() = {package.get_package_weight():.2f}\n"
)

except Exception as e:
print(e)
14 changes: 7 additions & 7 deletions docs/Secure-Coding-Guide-for-Python/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ It is __not production code__ and requires code-style or python best practices t
|[CWE-230: Improper Handling of Missing Values](CWE-703/CWE-230/.)||
|[CWE-390: Detection of Error Condition without Action](CWE-703/CWE-390/README.md)||
|[CWE-392: Missing Report of Error Condition](CWE-703/CWE-392/README.md)||
|[CWE-754: Improper Check for Unusual or Exceptional Conditions](CWE-703/CWE-754/.)||
|[CWE-754: Improper Check for Unusual or Exceptional Conditions - float](CWE-703/CWE-754/README.md)||
|[CWE-755: Improper Handling of Exceptional Conditions](CWE-703/CWE-755/README.md)|[CVE-2024-39560](https://www.cvedetails.com/cve/CVE-2024-39560),<br/>CVSSv3.1: __6.5__,<br/>EPSS: __0.04__ (01.11.2024)|

|[CWE-707: Improper Neutralization](https://cwe.mitre.org/data/definitions/707.html)|Prominent CVE|
Expand All @@ -111,12 +111,12 @@ It is __not production code__ and requires code-style or python best practices t

|Ref|Detail|
|-----|-----|
|[Python 2023]|3.9 Module Index [online], available from [https://docs.python.org/3.9/py-modindex.html](https://docs.python.org/3.9/py-modindex.html) [accessed Dec 2024]|
|[mitre.org 2023]|CWE - CWE-1000: Research Concepts [online], available from [https://cwe.mitre.org/data/definitions/1000.html](https://cwe.mitre.org/data/definitions/1000.html) [accessed Dec 2024]|
|[OWASP dev 2024]|OWASP Developer Guide [online], available from [https://owasp.org/www-project-developer-guide/release/](https://owasp.org/www-project-developer-guide/release/) [accessed Dec 2024]|
|[OWASP 2021]|OWASP Top 10 Report 2021 [online], available from [https://owasp.org/www-project-top-ten/](https://owasp.org/www-project-top-ten/)|
|[MITRE Pillar 2024]|_Pillar Weakness_ [online], available form [https://cwe.mitre.org/documents/glossary/#Pillar%20Weakness](https://cwe.mitre.org/documents/glossary/#Pillar%20Weakness) [accessed Dec 2024]|
|[MITRE 2024]|CWE Top 25 [online], available form [https://cwe.mitre.org/top25/index.html](https://cwe.mitre.org/top25/archive/2022/2022_cwe_top25.html) [accessed Dec 2024]|
|\[Python 2023\]|3.9 Module Index \[online\], available from [https://docs.python.org/3.9/py-modindex.html](https://docs.python.org/3.9/py-modindex.html) \[accessed Dec 2024\]|
|\[mitre.org 2023\]|CWE - CWE-1000: Research Concepts \[online\], available from [https://cwe.mitre.org/data/definitions/1000.html](https://cwe.mitre.org/data/definitions/1000.html) \[accessed Dec 2024\]|
|\[OWASP dev 2024\]|OWASP Developer Guide \[online\], available from [https://owasp.org/www-project-developer-guide/release/](https://owasp.org/www-project-developer-guide/release/) \[accessed Dec 2024\]|
|\[OWASP 2021\]|OWASP Top 10 Report 2021 \[online\], available from [https://owasp.org/www-project-top-ten/](https://owasp.org/www-project-top-ten/)|
|\[MITRE Pillar 2024\]|_Pillar Weakness_ \[online\], available form [https://cwe.mitre.org/documents/glossary/#Pillar%20Weakness](https://cwe.mitre.org/documents/glossary/#Pillar%20Weakness) \[accessed Dec 2024\]|
|\[MITRE 2024\]|CWE Top 25 \[online\], available form [https://cwe.mitre.org/top25/index.html](https://cwe.mitre.org/top25/archive/2022/2022_cwe_top25.html) \[accessed Dec 2024\]|

## License

Expand Down