Numeric Range Comparison Without Minimum Check
Description
Numeric Range Comparison Without Minimum Check is an input validation vulnerability where software validates that a numeric value does not exceed a maximum threshold but fails to verify that it meets a minimum threshold, such as being non-negative. This oversight is particularly dangerous when signed integers or floating-point numbers are used in contexts that expect only positive values. Negative values can cause unexpected behaviors including accessing memory before array boundaries, allocating unexpected amounts of memory, inverting financial transaction logic, or causing arithmetic overflow when converted to unsigned types.
Risk
This vulnerability can lead to buffer underflows, memory corruption, and business logic bypasses. When a negative array index is used, memory before the array is accessed, potentially leading to information disclosure or code execution. Negative values passed to memory allocation functions may wrap around to large values, causing allocation failures or excessive memory consumption. In financial applications, negative transaction amounts can invert the intended operation (withdrawal becomes deposit). When signed values are cast to unsigned, negative values become very large positive values, leading to massive buffer overflows or resource exhaustion.
Solution
Always validate both minimum and maximum bounds for numeric inputs. When only positive values are valid, explicitly check that values are greater than or equal to zero. Consider using unsigned integer types (size_t, unsigned int) when negative values are never valid. Be aware of implicit type conversions that can mask negative values. Implement validation at trust boundaries where external data enters the system. For array indices, validate against both 0 and the array length. For financial values, validate against both minimum and maximum transaction limits. Use assertions in debug builds to catch logic errors.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Modify Application Data - Negative values can cause unexpected data modifications or business logic inversions. |
| Availability | Scope: Availability DoS: Resource Consumption - Negative values may trigger excessive resource consumption when converted or used in calculations. |
| Confidentiality, Integrity | Scope: Confidentiality, Integrity Read/Modify Memory - Negative indices or sizes can access memory outside intended boundaries. |
Example Code
Vulnerable Code
// Vulnerable: No minimum check on packet header count
void vulnerable_parse_packet(packet_t *packet) {
int numHeaders = packet->headers;
// Only checks maximum, not minimum
if (numHeaders > MAX_HEADERS) {
error("Too many headers");
return;
}
// Vulnerable: Negative numHeaders causes problems
// When cast to size_t for malloc, becomes huge positive number
PacketHeader *headers = malloc(numHeaders * sizeof(PacketHeader));
// If allocation "succeeds" with corrupted size, overflow occurs
for (int i = 0; i < numHeaders; i++) {
parse_header(&headers[i], packet);
}
}
// Vulnerable: Array access without negative index check
int vulnerable_get_element(int *array, int length, int index) {
// Only checks upper bound
if (index < length) {
return array[index]; // Negative index reads before array!
}
return -1;
}
// Attack: index = -5 reads memory 5 positions before array
// Vulnerable: Financial withdrawal without negative check
public class VulnerableBank {
public boolean withdraw(Account account, double amount) {
// Only checks maximum withdrawal limit
if (amount > MAXIMUM_WITHDRAWAL_LIMIT) {
return false;
}
// Vulnerable: Negative amount causes deposit instead!
account.setBalance(account.getBalance() - amount);
// -(-100) = +100, so "withdrawing" -100 adds 100
return true;
}
}
# Vulnerable: Substring without negative length check
def vulnerable_substring(string, start, length):
# Only checks maximum length
if length > MAX_SUBSTRING_LENGTH:
length = MAX_SUBSTRING_LENGTH
# Vulnerable: Negative length or start causes issues
return string[start:start + length]
# Python handles this gracefully, but in C:
# memcpy(dest, src + start, length) with negative length is UB
// Vulnerable: Loop counter without negative check
void vulnerable_copy_elements(int *dest, int *src, int count) {
// Only checks maximum
if (count > MAX_ELEMENTS) {
count = MAX_ELEMENTS;
}
// Vulnerable: Negative count
// In unsigned comparison, -1 becomes huge positive number
for (size_t i = 0; i < (size_t)count; i++) {
dest[i] = src[i]; // Massive buffer overflow!
}
}
// Vulnerable: Array slice without negative validation
function vulnerableSlice(array, start, count) {
// Only validates count against array length
if (count > array.length) {
count = array.length;
}
// Vulnerable: Negative start or count
// JavaScript handles gracefully, but business logic may not
return array.slice(start, start + count);
}
// Vulnerable: Size used in memory allocation
int vulnerable_allocate(int requested_size) {
// Only checks maximum
if (requested_size > MAX_ALLOCATION_SIZE) {
return -1;
}
// Vulnerable: Negative size
// malloc((size_t)-1) requests ~4GB on 32-bit systems
char *buffer = malloc(requested_size);
if (!buffer) {
return -1;
}
memset(buffer, 0, requested_size); // Crash or corruption
return 0;
}
Fixed Code
// Fixed: Check both minimum and maximum bounds
void fixed_parse_packet(packet_t *packet) {
int numHeaders = packet->headers;
// Fixed: Check both bounds
if (numHeaders < 0 || numHeaders > MAX_HEADERS) {
error("Invalid header count");
return;
}
// Now safe to allocate
PacketHeader *headers = malloc((size_t)numHeaders * sizeof(PacketHeader));
if (!headers) {
error("Allocation failed");
return;
}
for (int i = 0; i < numHeaders; i++) {
parse_header(&headers[i], packet);
}
}
// Fixed: Validate array index range
int fixed_get_element(int *array, int length, int index) {
// Fixed: Check both bounds
if (index < 0 || index >= length) {
return -1; // Invalid index
}
return array[index];
}
// Better: Use unsigned types when negative values are invalid
int better_get_element(int *array, size_t length, size_t index) {
// size_t is unsigned, so negative values are impossible
if (index >= length) {
return -1;
}
return array[index];
}
// Fixed: Validate transaction amount bounds
public class FixedBank {
private static final double MINIMUM_TRANSACTION = 0.01;
public boolean withdraw(Account account, double amount) {
// Fixed: Check both minimum and maximum
if (amount < MINIMUM_TRANSACTION) {
return false; // Rejects negative and zero amounts
}
if (amount > MAXIMUM_WITHDRAWAL_LIMIT) {
return false;
}
if (amount > account.getBalance()) {
return false; // Insufficient funds
}
account.setBalance(account.getBalance() - amount);
return true;
}
}
# Fixed: Validate both bounds for substring
def fixed_substring(string, start, length):
# Fixed: Validate all parameters
if start < 0:
start = 0
if length < 0:
return "" # Or raise ValueError
if start > len(string):
return ""
# Cap at string end
end = min(start + length, len(string))
return string[start:end]
# Better: Use explicit validation with exceptions
def better_substring(string, start, length):
if not isinstance(start, int) or not isinstance(length, int):
raise TypeError("start and length must be integers")
if start < 0 or length < 0:
raise ValueError("start and length must be non-negative")
if start > len(string):
raise IndexError("start exceeds string length")
return string[start:start + length]
// Fixed: Use unsigned type for count
void fixed_copy_elements(int *dest, int *src, size_t count) {
// Fixed: Using size_t means negative values are impossible
// But still validate maximum
if (count > MAX_ELEMENTS) {
count = MAX_ELEMENTS;
}
// Safe: count is guaranteed non-negative
for (size_t i = 0; i < count; i++) {
dest[i] = src[i];
}
}
// When signed type is required by API:
void fixed_copy_signed(int *dest, int *src, int count) {
// Fixed: Explicit validation
if (count < 0) {
return; // Invalid count
}
if (count > MAX_ELEMENTS) {
count = MAX_ELEMENTS;
}
for (int i = 0; i < count; i++) {
dest[i] = src[i];
}
}
// Fixed: Proper size validation for allocation
int fixed_allocate(int requested_size) {
// Fixed: Check both bounds
if (requested_size < 0) {
return -1; // Invalid size
}
if (requested_size > MAX_ALLOCATION_SIZE) {
return -1;
}
// Safe cast: we've verified non-negative
char *buffer = malloc((size_t)requested_size);
if (!buffer) {
return -1;
}
memset(buffer, 0, (size_t)requested_size);
return 0;
}
// Better: Use size_t from the start
int better_allocate(size_t requested_size) {
// size_t is unsigned, negative values impossible
if (requested_size > MAX_ALLOCATION_SIZE) {
return -1;
}
char *buffer = malloc(requested_size);
if (!buffer) {
return -1;
}
memset(buffer, 0, requested_size);
return 0;
}
// Fixed: Comprehensive validation utility
public class NumericValidator {
public static void validateInRange(int value, int min, int max, String name) {
if (value < min || value > max) {
throw new IllegalArgumentException(
String.format("%s must be between %d and %d, got %d",
name, min, max, value));
}
}
public static void validateNonNegative(int value, String name) {
if (value < 0) {
throw new IllegalArgumentException(
name + " must be non-negative, got " + value);
}
}
public static void validatePositive(int value, String name) {
if (value <= 0) {
throw new IllegalArgumentException(
name + " must be positive, got " + value);
}
}
}
// Usage:
public void processData(int count, int index) {
NumericValidator.validateNonNegative(count, "count");
NumericValidator.validateInRange(index, 0, count - 1, "index");
// Now safe to use
}
Related CWEs
- CWE-1023: Incomplete Comparison with Missing Factors (parent)
- CWE-119: Improper Restriction of Operations within Bounds of Memory Buffer (can follow)
- CWE-124: Buffer Underwrite ('Buffer Underflow') (can follow)
- CWE-195: Signed to Unsigned Conversion Error (can follow)
- CWE-129: Improper Validation of Array Index (related)
References
- MITRE Corporation. "CWE-839: Numeric Range Comparison Without Minimum Check." https://cwe.mitre.org/data/definitions/839.html
- CERT C Secure Coding Standard. "INT04-C. Enforce limits on integer values originating from tainted sources."
- CERT C Secure Coding Standard. "ARR30-C. Do not form or use out-of-bounds pointers or array subscripts."