Skip to content

CWE-129: Improper Validation of Array Index

Overview

Improper validation of array index occurs when user-controlled or untrusted data is used as an array index without proper bounds checking, allowing attackers to read or write arbitrary memory locations, leak sensitive data, or cause crashes.

OWASP Classification

A05:2025 - Injection

Risk

High: Unvalidated array indices enable attackers to read sensitive memory (passwords, keys), overwrite critical data structures, bypass security checks, or achieve arbitrary code execution. In web applications, this can enable unauthorized data access or privilege escalation.

Remediation Steps

Core principle: Validate array indices before use, including negative and overflowed values.

Locate the unvalidated array index in your code

  • Review the security findings to identify where untrusted data is used as an array index
  • Find the array access: identify which array or buffer is accessed with untrusted index
  • Identify the index source: determine where the index comes from (user input, external file, database, network request)
  • Trace the data flow: review how the index flows from source to array access

Validate all indices before use (Primary Defense)

  • Check both bounds: Ensure index >= 0 && index < array.length before access
  • Use unsigned types: For indices, use size_t (C/C++) or unsigned int to prevent negative values
  • Reject invalid indices: Don't silently clamp to bounds - reject and return error
  • Validate early: Check indices as soon as they're received from untrusted sources
  • Don't trust any external data: Treat all indices from untrusted data, files, or network as potentially malicious

Use safe array access methods with automatic bounds checking

  • C++ at() method: Use vector.at(index) instead of [] - throws exception on out-of-bounds
  • Java/C# automatic checks: These languages throw ArrayIndexOutOfBoundsException automatically
  • Use iterators: Prefer range-based for loops or iterators over manual indexing
  • Enable runtime checks: Use debug builds with bounds checking enabled
  • Avoid pointer arithmetic: Don't manually calculate offsets - use safe abstractions

Prevent integer overflow in index calculations

  • Validate before arithmetic: Check offset + index won't overflow before calculation
  • Check for overflow: Use safe math libraries or manual overflow checking
  • Validate calculated indices: After computing index, validate it's still within bounds
  • Watch for multiplication: Be especially careful with index * element_size calculations
  • Use wider types: Perform calculations in larger type (int64) then validate before using

Apply defense in depth with multiple protection layers

  • Input validation: validate untrusted data format, type, and range before using as index
  • Array bounds checking: always validate indices against array size
  • Memory safety: enable ASLR, DEP to make exploitation harder
  • Static analysis: use tools to find unchecked array accesses
  • Logging: log invalid index attempts for security monitoring

Test with malicious indices

  • Test with negative values: try -1, -100 (should be rejected)
  • Test with very large values: try MAX_INT, array_size + 1000 (should be rejected)
  • Test with exact boundary: try index = array_size (should be rejected, valid is 0 to size-1)
  • Test with overflow: try index calculations that overflow (offset + index > MAX_INT)
  • Test with valid indices: ensure legitimate array access still works

Common Vulnerable Patterns

Using user input directly as array index in Java

public class VulnerableArray {
    private static final String[] COLORS = {
        "red", "green", "blue", "yellow"
    };

    public String getColor(int index) {
        // VULNERABLE - no validation, index comes from user input
        return COLORS[index];

        // Attack: index = -1 (reads before array, or throws exception)
        // Attack: index = 100 (reads past array, crashes app)
        // Result: ArrayIndexOutOfBoundsException or memory disclosure
    }

    public int[] processUserData(int[] userIndices) {
        int[] results = new int[10];

        // VULNERABLE - trusts user-provided array indices
        for (int i = 0; i < userIndices.length; i++) {
            results[userIndices[i]] = calculateValue(i);
        }

        // Attack: userIndices = [-5, 15, 1000]
        // Result: ArrayIndexOutOfBoundsException, app crash

        return results;
    }
}

Unvalidated index in C allowing negative access

#include <stdio.h>

void lookup_value(int user_index) {
    int lookup[] = {10, 20, 30, 40, 50};

    // VULNERABLE - no bounds check, negative index not checked
    int value = lookup[user_index];

    // Attack: user_index = -1
    // Result: reads 4 bytes before lookup array
    // Could leak sensitive data from stack

    // Attack: user_index = 100
    // Result: reads memory 400 bytes past array
    // Information disclosure or crash

    printf("Value: %d\n", value);
}

void access_buffer(char *buffer, int offset) {
    // VULNERABLE - negative offset can read before buffer
    char data = buffer[offset];

    // Attack: offset = -50
    // Result: reads 50 bytes before buffer start
    // Could expose passwords, keys, other sensitive stack data
}

Missing validation in write operations

public class ScoreSystem {
    private int[] playerScores = new int[10];

    public void updateScore(int playerId, int points) {
        // VULNERABLE - playerId not validated, can write anywhere
        playerScores[playerId] += points;

        // Attack: playerId = -1
        // Result: writes before array, corrupts adjacent memory

        // Attack: playerId = 50
        // Result: ArrayIndexOutOfBoundsException, DOS
    }

    public void setPermissions(int userId, boolean isAdmin) {
        boolean[] adminFlags = new boolean[100];

        // VULNERABLE - can overwrite arbitrary boolean
        adminFlags[userId] = isAdmin;

        // Attack: userId = 99999
        // Result: writes far past array, heap corruption
        // Potential privilege escalation
    }
}

Integer overflow in index calculation

#include <stdint.h>

void process_record(uint8_t *data, size_t data_size, 
                    uint32_t record_id, uint32_t record_size) {
    // VULNERABLE - record_id * record_size can overflow
    size_t offset = record_id * record_size;

    // VULNERABLE - even if no overflow, offset not validated
    uint8_t record_type = data[offset];

    // Attack: record_id = 0x10000000, record_size = 16
    // Calculation: 0x10000000 * 16 = 0x100000000 (overflows to 0)
    // Result: offset = 0, reads wrong record

    // Attack: record_id = 1000, record_size = 100, data_size = 1000
    // offset = 100000, far exceeds data_size
    // Result: reads 99000 bytes past buffer
}

void lookup_with_offset(int *array, size_t size, int base, int offset) {
    // VULNERABLE - base + offset can overflow
    int index = base + offset;

    // VULNERABLE - negative overflow not checked
    int value = array[index];

    // Attack: base = 0x7FFFFFFF, offset = 10
    // index = 0x80000009 (overflows to large negative)
    // Result: reads before array start
}

## Secure Patterns

### Validating indices before array access in Java

```java
public class SecureArray {
    private static final String[] COLORS = {
        "red", "green", "blue", "yellow"
    };

    public String getColor(int index) {
        // Validate index before access
        if (index < 0 || index >= COLORS.length) {
            throw new IllegalArgumentException(
                "Invalid color index: " + index + 
                " (valid range: 0-" + (COLORS.length - 1) + ")");
        }

        // Safe: validated index
        return COLORS[index];
    }

    public int[] processUserData(int[] userIndices) {
        int[] results = new int[10];

        // Validate each index before use
        for (int i = 0; i < userIndices.length; i++) {
            int idx = userIndices[i];

            // Check bounds
            if (idx < 0 || idx >= results.length) {
                throw new IllegalArgumentException(
                    "Invalid index at position " + i + ": " + idx);
            }

            // Safe: validated index
            results[idx] = calculateValue(i);
        }

        return results;
    }
}

Why this works: Explicit validation (index < 0 || index >= COLORS.length) checks both negative and out-of-bounds indices before array access, preventing ArrayIndexOutOfBoundsException and ensuring only valid indices reach the array. The check happens before the array access, so malicious indices never touch memory. Throwing meaningful exceptions with details helps debugging and prevents silent failures. This pattern works in all Java contexts and prevents both read and write operations with invalid indices.

Comprehensive validation in C

#include <stdio.h>
#include <stddef.h>

int lookup_value(int user_index) {
    int lookup[] = {10, 20, 30, 40, 50};
    size_t lookup_size = sizeof(lookup) / sizeof(lookup[0]);

    // Check for negative index first
    if (user_index < 0) {
        fprintf(stderr, "Negative index not allowed: %d\n", user_index);
        return -1;
    }

    // Check upper bound
    if ((size_t)user_index >= lookup_size) {
        fprintf(stderr, "Index %d out of bounds (max: %zu)\n", 
                user_index, lookup_size - 1);
        return -1;
    }

    // Safe: both bounds validated
    return lookup[user_index];
}

void access_buffer(const char *buffer, size_t buffer_size, int offset) {
    // Validate buffer pointer
    if (buffer == NULL) {
        fprintf(stderr, "Null buffer pointer\n");
        return;
    }

    // Validate offset is non-negative
    if (offset < 0) {
        fprintf(stderr, "Negative offset not allowed: %d\n", offset);
        return;
    }

    // Validate offset within buffer bounds
    if ((size_t)offset >= buffer_size) {
        fprintf(stderr, "Offset %d exceeds buffer size %zu\n",
                offset, buffer_size);
        return;
    }

    // Safe: validated offset
    char data = buffer[offset];
    process(data);
}

Why this works: Separate checks for negative values (user_index < 0) and upper bounds (user_index >= lookup_size) provide comprehensive validation against all invalid indices. Using size_t for array sizes prevents sign-related errors in comparisons. Checking for null pointers prevents dereferencing invalid memory. Casting to size_t for comparison ensures correct handling of the conversion from signed to unsigned. This defense-in-depth approach catches negative indices (reading before array), positive out-of-bounds (reading past array), and null pointer errors.

Validating write operations

public class SecureScoreSystem {
    private int[] playerScores = new int[10];

    public void updateScore(int playerId, int points) {
        // Validate playerId before write
        if (playerId < 0 || playerId >= playerScores.length) {
            throw new IllegalArgumentException(
                "Invalid player ID: " + playerId + 
                " (valid range: 0-" + (playerScores.length - 1) + ")");
        }

        // Validate points to prevent integer overflow
        if (points < 0 || points > Integer.MAX_VALUE - playerScores[playerId]) {
            throw new IllegalArgumentException("Points value would cause overflow");
        }

        // Safe: validated index and value
        playerScores[playerId] += points;
    }

    public void setPermissions(int userId, boolean isAdmin) {
        boolean[] adminFlags = new boolean[100];

        // Strict validation before permission change
        if (userId < 0) {
            throw new IllegalArgumentException("User ID cannot be negative");
        }

        if (userId >= adminFlags.length) {
            throw new IllegalArgumentException(
                "User ID " + userId + " exceeds maximum (" + 
                (adminFlags.length - 1) + ")");
        }

        // Log security-sensitive operation
        auditLog("Permission change: user=" + userId + ", admin=" + isAdmin);

        // Safe: validated index, logged operation
        adminFlags[userId] = isAdmin;
    }
}

Why this works: Validating indices before write operations is critical because writes can corrupt memory, not just read it. The playerId validation prevents writing to arbitrary memory locations. Additional validation of the points value prevents integer overflow in the accumulation operation. Logging security-sensitive operations (permission changes) provides an audit trail. Separate validation for negative and positive bounds provides defense-in-depth. This pattern is essential for security-critical operations like permission management.

Preventing integer overflow in index calculations

#include <stdint.h>
#include <limits.h>
#include <stdio.h>

void process_record(const uint8_t *data, size_t data_size, 
                    uint32_t record_id, uint32_t record_size) {
    // Validate inputs are reasonable
    if (record_size == 0 || record_size > 1024) {
        fprintf(stderr, "Invalid record size: %u\n", record_size);
        return;
    }

    // Check for multiplication overflow BEFORE calculating
    if (record_id > SIZE_MAX / record_size) {
        fprintf(stderr, "Record ID too large, would overflow\n");
        return;
    }

    // Safe: overflow checked
    size_t offset = (size_t)record_id * record_size;

    // Validate offset is within buffer bounds
    if (offset >= data_size) {
        fprintf(stderr, "Offset %zu exceeds data size %zu\n",
                offset, data_size);
        return;
    }

    // Safe: validated offset
    uint8_t record_type = data[offset];
    process(record_type);
}

void lookup_with_offset(const int *array, size_t size, int base, int offset) {
    // Check for overflow in addition
    if (base < 0 || offset < 0) {
        fprintf(stderr, "Negative indices not allowed\n");
        return;
    }

    // Check if base + offset would overflow
    if ((size_t)base > size || (size_t)offset > size - base) {
        fprintf(stderr, "Index calculation would exceed array bounds\n");
        return;
    }

    // Safe: overflow prevented
    size_t index = (size_t)base + (size_t)offset;

    // Double-check bounds
    if (index >= size) {
        fprintf(stderr, "Calculated index out of bounds\n");
        return;
    }

    // Safe: validated index
    int value = array[index];
    process(value);
}

Why this works: Checking for multiplication overflow (record_id > SIZE_MAX / record_size) before performing the calculation prevents the overflow from occurring by dividing the maximum value by one operand and checking if the other operand exceeds the result. For addition, the rearranged check (offset > size - base) prevents overflow by using subtraction instead of addition. Validating reasonable input ranges (e.g., record_size > 1024) catches suspiciously large values. Double-checking the final calculated index provides defense-in-depth. This pattern is essential when working with untrusted size/offset values from network protocols or file formats.

Using C++ containers with automatic bounds checking

#include <vector>
#include <stdexcept>
#include <iostream>

class SecureArray {
private:
    std::vector<int> data;

public:
    SecureArray() : data{10, 20, 30, 40, 50} {}

    int getValue(size_t index) {
        // Use at() for automatic bounds checking
        try {
            return data.at(index);
        } catch (const std::out_of_range& e) {
            throw std::out_of_range(
                "Index " + std::to_string(index) + 
                " out of bounds (size: " + std::to_string(data.size()) + ")");
        }
    }

    // Manual validation for signed indices
    int getValueSigned(int index) {
        // Check for negative index
        if (index < 0) {
            throw std::out_of_range("Negative index not allowed");
        }

        // Check upper bound
        if (static_cast<size_t>(index) >= data.size()) {
            throw std::out_of_range(
                "Index " + std::to_string(index) + 
                " out of bounds (size: " + std::to_string(data.size()) + ")");
        }

        return data[index];
    }

    // Safe iteration without indexing
    void processAll() {
        // Range-based for loop - no index arithmetic
        for (const auto& value : data) {
            process(value);
        }
    }

    // Safe indexed iteration
    void processWithIndex() {
        // Loop condition uses size(), always safe
        for (size_t i = 0; i < data.size(); i++) {
            process(data[i]);  // i is always < data.size()
        }
    }
};

Why this works: The at() method performs automatic runtime bounds checking and throws std::out_of_range exception on invalid access, preventing out-of-bounds reads/writes without requiring manual validation. std::vector tracks its own size via .size(), eliminating manual size tracking errors. Range-based for loops eliminate index arithmetic entirely, preventing index calculation errors. When manual validation is needed (e.g., for signed integers), separate checks for negative values and upper bounds provide comprehensive protection. Using size_t for loop counters matches the container's size type, preventing signedness issues. C++ containers provide memory safety with minimal performance overhead compared to manual bounds checking.

Additional Resources