|
| 1 | +# CWE-754: Improper Check for Unusual or Exceptional Conditions - Float |
| 2 | + |
| 3 | +Ensure to have handling for exceptional floating-point values. |
| 4 | + |
| 5 | +The `float` class has the capability to interpret various input values as floating-point numbers. Some special cases can interpret input values as |
| 6 | + |
| 7 | +* Positive Infinity |
| 8 | +* Negative Infinity |
| 9 | +* `NaN` (Not-a-Number) |
| 10 | + |
| 11 | +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`. |
| 12 | + |
| 13 | +## Non-Compliant Code Example |
| 14 | + |
| 15 | +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. |
| 16 | + |
| 17 | +*[noncompliant01.py](noncompliant01.py):* |
| 18 | + |
| 19 | +```py |
| 20 | +# SPDX-FileCopyrightText: OpenSSF project contributors |
| 21 | +# SPDX-License-Identifier: MIT |
| 22 | +"""Non-compliant Code Example""" |
| 23 | + |
| 24 | +import sys |
| 25 | + |
| 26 | + |
| 27 | +class Package: |
| 28 | + """Class representing a package object""" |
| 29 | + |
| 30 | + def __init__(self): |
| 31 | + self.package_weight: float = 0.0 |
| 32 | + self.max_package_weight: float = 100.0 |
| 33 | + |
| 34 | + def add_to_package(self, object_weight: str): |
| 35 | + """Function for adding an object into the package""" |
| 36 | + value = float(object_weight) |
| 37 | + # This is dead code as value gets type cast to float, |
| 38 | + # hence will never be equal to string "NaN" |
| 39 | + if value == "NaN": |
| 40 | + raise ValueError("'NaN' not a number") |
| 41 | + # This is also dead code as value is type cast to float, |
| 42 | + # unusual inputs like -infinity will not get caught |
| 43 | + if isinstance(value, float) is False: |
| 44 | + raise ValueError("not a number") |
| 45 | + if self.package_weight + value > self.max_package_weight: |
| 46 | + raise ValueError("Addition would exceed maximum package weight.") |
| 47 | + |
| 48 | + self.package_weight += value |
| 49 | + |
| 50 | + def get_package_weight(self): |
| 51 | + """Getter for outputting the package's current weight""" |
| 52 | + return self.package_weight |
| 53 | + |
| 54 | + |
| 55 | +##################### |
| 56 | +# exploiting above code example |
| 57 | +##################### |
| 58 | +package = Package() |
| 59 | +print(f"\nOriginal package's weight is {package.get_package_weight():.2f} units\n") |
| 60 | +for item in [100, "-infinity", sys.float_info.max, "NaN", -100]: |
| 61 | + print(f"package.add_to_package({item})") |
| 62 | + try: |
| 63 | + package.add_to_package(item) |
| 64 | + print( |
| 65 | + f"package.get_package_weight() = {package.get_package_weight():.2f}\n" |
| 66 | + ) |
| 67 | + |
| 68 | + except Exception as e: |
| 69 | + print(e) |
| 70 | +``` |
| 71 | + |
| 72 | +Some important considerations when dealing with floating-point values from `non-complaint01.py`. |
| 73 | + |
| 74 | +* 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`. |
| 75 | +* Setting the added value to `-infinity` and `+infinity` causes the value of the `package_weight` to be infinite as well. |
| 76 | +* Adding `"NaN"`, which is not a valid value to `package_weight` will always return `"nan"`. |
| 77 | + |
| 78 | +**Example `noncompliant01.py` output:** |
| 79 | + |
| 80 | +```bash |
| 81 | +Original package's weight is 0.00 units |
| 82 | +
|
| 83 | +package.add_to_package(100) |
| 84 | +package.get_package_weight() = 100.00 |
| 85 | +
|
| 86 | +package.add_to_package(-infinity) |
| 87 | +package.get_package_weight() = -inf |
| 88 | +
|
| 89 | +package.add_to_package(1.7976931348623157e+308) |
| 90 | +package.get_package_weight() = -inf |
| 91 | +
|
| 92 | +package.add_to_package(NaN) |
| 93 | +package.get_package_weight() = nan |
| 94 | +
|
| 95 | +package.add_to_package(-100) |
| 96 | +package.get_package_weight() = nan |
| 97 | +``` |
| 98 | +
|
| 99 | +## Compliant Solution |
| 100 | +
|
| 101 | +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. |
| 102 | +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`. |
| 103 | +
|
| 104 | +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). |
| 105 | +
|
| 106 | +*[compliant01.py](compliant01.py):* |
| 107 | +
|
| 108 | +```py |
| 109 | +# SPDX-FileCopyrightText: OpenSSF project contributors |
| 110 | +# SPDX-License-Identifier: MIT |
| 111 | +""" Compliant Code Example """ |
| 112 | +
|
| 113 | +
|
| 114 | +import sys |
| 115 | +from math import isfinite, isnan |
| 116 | +from typing import Union |
| 117 | +
|
| 118 | +
|
| 119 | +class Package: |
| 120 | + """Class representing a package object.""" |
| 121 | + def __init__(self) -> None: |
| 122 | + self.package_weight: float = 0.0 |
| 123 | + self.max_package_weight: float = 100.0 |
| 124 | +
|
| 125 | + def add_to_package(self, object_weight: Union[str, int, float]) -> None: |
| 126 | + # TODO: input sanitation. |
| 127 | + # TODO: proper exception handling |
| 128 | + """Add an object into the package after validating its weight.""" |
| 129 | + try: |
| 130 | + value = float(object_weight) |
| 131 | + except (ValueError, TypeError) as e: |
| 132 | + raise ValueError("Input cannot be converted to a float.") from e |
| 133 | + if isnan(value): |
| 134 | + raise ValueError("Input is not a number") |
| 135 | + if not isfinite(value): |
| 136 | + raise ValueError("Input is not a finite number.") |
| 137 | + if value < 0: |
| 138 | + raise ValueError("Weight must be a non-negative number.") |
| 139 | + if self.package_weight + value > self.max_package_weight: |
| 140 | + raise ValueError("Addition would exceed maximum package weight.") |
| 141 | + |
| 142 | + print(f"Adding an object that weighs {value} units to package") |
| 143 | + self.package_weight += value |
| 144 | + |
| 145 | + def get_package_weight(self) -> float: |
| 146 | + """Return the package's current weight.""" |
| 147 | + return self.package_weight |
| 148 | +
|
| 149 | +
|
| 150 | +##################### |
| 151 | +# exploiting above code example |
| 152 | +##################### |
| 153 | + |
| 154 | + |
| 155 | +package = Package() |
| 156 | +print(f"\nOriginal package's weight is {package.get_package_weight():.2f} units\n") |
| 157 | +for item in [100, "-infinity", sys.float_info.max, "NaN", -100]: |
| 158 | + print(f"package.add_to_package({item})") |
| 159 | + try: |
| 160 | + package.add_to_package(item) |
| 161 | + print( |
| 162 | + f"package.get_package_weight() = {package.get_package_weight():.2f}\n" |
| 163 | + ) |
| 164 | + |
| 165 | + except Exception as e: |
| 166 | + print(e) |
| 167 | +``` |
| 168 | +
|
| 169 | +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. |
| 170 | +
|
| 171 | +**Example `compliant01.py` output:** |
| 172 | +
|
| 173 | +```bash |
| 174 | +Original package's weight is 0.00 units |
| 175 | + |
| 176 | +package.add_to_package(100) |
| 177 | +Adding an object that weighs 100.0 units to package |
| 178 | +package.get_package_weight() = 100.00 |
| 179 | + |
| 180 | +package.add_to_package(-infinity) |
| 181 | +Input is not a finite number. |
| 182 | +package.add_to_package(1.7976931348623157e+308) |
| 183 | +Addition would exceed maximum package weight. |
| 184 | +package.add_to_package(NaN) |
| 185 | +Input is not a number |
| 186 | +package.add_to_package(-100) |
| 187 | +Weight must be a non-negative number. |
| 188 | +``` |
| 189 | + |
| 190 | +## Automated Detection |
| 191 | + |
| 192 | +||||| |
| 193 | +|:---|:---|:---|:---| |
| 194 | +|Tool|Version|Checker|Description| |
| 195 | +|bandit|1.7.4|no detection|| |
| 196 | + |
| 197 | +## Related Guidelines |
| 198 | + |
| 199 | +||| |
| 200 | +|:---|:---| |
| 201 | +|[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)| |
| 202 | +|[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)| |
| 203 | +|[CWE MITRE Pillar](http://cwe.mitre.org/)|[CWE-703: Improper Check or Handling of Exceptional Conditions](https://cwe.mitre.org/data/definitions/703.html)| |
| 204 | +|[MITRE CWE Base](https://cwe.mitre.org/)|[CWE-754: Improper Check for Unusual or Exceptional Conditions](https://cwe.mitre.org/data/definitions/754.html)| |
0 commit comments