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

ImpactDetails
IntegrityScope: Integrity

Modify Application Data - Negative values can cause unexpected data modifications or business logic inversions.
AvailabilityScope: Availability

DoS: Resource Consumption - Negative values may trigger excessive resource consumption when converted or used in calculations.
Confidentiality, IntegrityScope: 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
}

  • 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

  1. MITRE Corporation. "CWE-839: Numeric Range Comparison Without Minimum Check." https://cwe.mitre.org/data/definitions/839.html
  2. CERT C Secure Coding Standard. "INT04-C. Enforce limits on integer values originating from tainted sources."
  3. CERT C Secure Coding Standard. "ARR30-C. Do not form or use out-of-bounds pointers or array subscripts."