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.lengthbefore access - Use unsigned types: For indices, use
size_t(C/C++) orunsigned intto 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 + indexwon'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_sizecalculations - 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.