CWE-118: Incorrect Access of Indexable Resource (Range Error)
Overview
Incorrect access of indexable resources occurs when code accesses memory, arrays, or buffers outside their valid bounds, either through buffer overflows, underflows, or improper index calculations. This is particularly dangerous in languages like C/C++ without automatic bounds checking.
Risk
Critical: Buffer access violations can lead to arbitrary code execution, memory corruption, information disclosure, or application crashes. Attackers can exploit these to bypass security controls, escalate privileges, or completely compromise the system.
Remediation Steps
Core principle: Maintain clear ownership and validation boundaries for data; avoid implicit trust transfer between components.
Identify the Bounds Violation
Review the security findings to understand the indexable resource access issue:
- Locate the vulnerability: Find the specific array, buffer, or memory access in the code
- Identify the index source: Determine where the index/offset comes from (untrusted data, calculation, loop variable)
- Check the resource bounds: Understand the valid range for the indexable resource
- Trace the data flow: Review how the index is calculated or obtained
Use Safe APIs and Data Structures
Replace unsafe operations with bounds-checked alternatives:
- C++ STL containers: Use
std::vector,std::string,std::arrayinstead of raw arrays/pointers - Safe C functions: Replace
strcpywithstrncpy,sprintfwithsnprintf,strcatwithstrncat - Bounds-checked methods: Use
.at()method in C++ which throws on out-of-bounds access - Consider managed languages: If feasible, migrate to Java, C#, Python which have automatic bounds checking
- Use safe wrappers: Consider using SafeInt or similar libraries for bounds-checked operations
Validate All Array Indices
Add explicit bounds checking before accessing indexable resources:
- Check against bounds: Ensure
index >= 0 && index < array_sizebefore access - Validate untrusted indices explicitly: If index comes from untrusted data, validate before use
- Use unsigned types: Use
size_torunsigned intfor indices to prevent negative indexing attacks - Reject out-of-bounds access: Return error or throw exception instead of accessing invalid indices
- Off-by-one checks: Ensure loop conditions use
<not<=for array sizes
Calculate Buffer Sizes Correctly
Ensure buffer allocations and operations account for all data:
- Account for null terminators: For C strings, allocate
length + 1for the\0terminator - Use sizeof() correctly: Use
sizeof(buffer)not hardcoded sizes for static buffers - Track dynamic sizes: Explicitly store and check sizes for dynamically allocated buffers
- Verify before copy: Check
source_size < dest_sizebefore copy operations - Check integer overflow: Ensure buffer size calculations don't overflow
Enable Compiler Protections and Testing Tools
Use development tools to detect bounds violations:
- Enable AddressSanitizer: Compile with
-fsanitize=address(GCC/Clang) to detect memory errors - Use Valgrind: Run tests under Valgrind to find memory access violations
- Enable stack canaries: Compile with
-fstack-protector-strongto detect stack corruption - Enable DEP/ASLR: Use address space layout randomization and data execution prevention
- Use static analysis: Run Static Analysis tools regularly as part of your SDLC
Test with Boundary Conditions
Verify the fix handles edge cases:
- Test with maximum indices: Try accessing last valid element, and one past it (should be rejected)
- Test with zero/negative: If using signed types, test negative indices (should be rejected)
- Test with oversized data: Pass strings/arrays larger than buffers can hold
- Test off-by-one: Verify loops don't access one past the end
- Fuzz test: Use fuzzing tools (AFL, libFuzzer) to find edge cases
Common Vulnerable Patterns
Using strcpy without bounds checking
#include <string.h>
void process_input(char *user_input) {
char buffer[100];
// VULNERABLE - no bounds checking, buffer overflow if user_input > 100 bytes
strcpy(buffer, user_input);
// Attack: user_input with 200 bytes overwrites adjacent memory
// Result: memory corruption, possible code execution
}
void use_sprintf() {
char buffer[50];
char *name = get_user_name(); // Untrusted input
// VULNERABLE - sprintf doesn't check buffer size
sprintf(buffer, "Hello, %s!", name);
// Attack: long name causes buffer overflow
}
Unchecked array index from user input
void bad_array_access(int user_index) {
int data[10];
// VULNERABLE - no validation, user_index could be negative or >= 10
data[user_index] = 42;
// Attack: user_index = -1 (writes before array)
// Attack: user_index = 100 (writes past array end)
// Result: memory corruption
}
void access_string_buffer(int position) {
char buffer[20] = "Some data";
// VULNERABLE - no bounds check
char ch = buffer[position];
// Attack: position = 50 (reads past buffer)
// Result: information disclosure
}
Off-by-one error in loop
void bad_loop() {
char buffer[10];
// VULNERABLE - <= instead of <, accesses buffer[10] which is out of bounds
for (int i = 0; i <= 10; i++) {
buffer[i] = 'A';
}
// Attack: writes to buffer[10], corrupts adjacent memory
}
void copy_with_loop(char *src) {
char dest[100];
int i;
// VULNERABLE - no check if src fits in dest
for (i = 0; src[i] != '\0'; i++) {
dest[i] = src[i]; // Could write past dest[99]
}
dest[i] = '\0';
}
Missing null terminator space
void allocate_string() {
char *name = "Alice";
int len = strlen(name); // len = 5
// VULNERABLE - allocated exactly 5 bytes, no room for null terminator
char *buffer = (char *)malloc(len);
strcpy(buffer, name); // Writes 6 bytes (5 + '\0'), overflows by 1
// Attack: heap corruption
}
void fixed_size_copy(const char *input) {
char buffer[10];
// VULNERABLE - strncpy doesn't guarantee null termination
strncpy(buffer, input, 10);
// If input is >= 10 chars, buffer is NOT null-terminated
printf("%s\n", buffer); // Could read past buffer end
}
Secure Patterns
Using strncpy with explicit null termination
#include <string.h>
#include <stdio.h>
#define MAX_BUFFER 100
void process_input(const char *user_input) {
char buffer[MAX_BUFFER];
// Safe: bounds-checked copy with size limit
strncpy(buffer, user_input, MAX_BUFFER - 1);
buffer[MAX_BUFFER - 1] = '\0'; // Always ensure null termination
// strncpy copies at most MAX_BUFFER - 1 characters
// Explicit null terminator prevents reading past buffer end
}
Why this works: strncpy(buffer, source, MAX_BUFFER - 1) limits the copy to at most MAX_BUFFER - 1 bytes, preventing writes past the buffer boundary. The explicit buffer[MAX_BUFFER - 1] = '\0' ensures the string is always null-terminated, even if the source was longer than the buffer (since strncpy doesn't add a null terminator when truncating). This prevents reading past the buffer end when the string is later used.
Using snprintf for safe formatted output
#include <stdio.h>
void format_message(const char *user_name) {
char buffer[50];
// Safe: snprintf limits output size and guarantees null termination
snprintf(buffer, sizeof(buffer), "Hello, %s!", user_name);
// snprintf will truncate if needed and always null-terminate
// Returns number of chars that would be written (excluding null)
}
void build_path(const char *dir, const char *file) {
char path[256];
// Safe: multiple inputs, size controlled
int written = snprintf(path, sizeof(path), "%s/%s", dir, file);
// Check if truncation occurred
if (written >= sizeof(path)) {
fprintf(stderr, "Path too long\n");
return;
}
// Use path safely
}
Why this works: snprintf() automatically limits output to the specified buffer size and guarantees null termination, unlike sprintf() which has no bounds checking. It returns the number of characters that would have been written (excluding the null terminator), allowing detection of truncation. Using sizeof(buffer) instead of hardcoded sizes ensures the limit matches the actual buffer size, preventing errors when buffer sizes change.
Validating array indices before access
#include <stdio.h>
#define ARRAY_SIZE 10
void safe_array_access(int user_index) {
int data[ARRAY_SIZE];
// Validate index is within valid range
if (user_index < 0 || user_index >= ARRAY_SIZE) {
fprintf(stderr, "Invalid index: %d (valid range: 0-%d)\n",
user_index, ARRAY_SIZE - 1);
return;
}
// Safe: index validated before access
data[user_index] = 42;
}
void safe_string_access(const char *str, int position) {
if (str == NULL) {
fprintf(stderr, "Null pointer\n");
return;
}
size_t len = strlen(str);
// Validate position is within string bounds
if (position < 0 || position >= len) {
fprintf(stderr, "Position %d out of range (0-%zu)\n", position, len - 1);
return;
}
// Safe: validated access
char ch = str[position];
printf("Character at %d: %c\n", position, ch);
}
Why this works: Explicit bounds checking (user_index >= 0 && user_index < ARRAY_SIZE) before array access ensures the index is within the valid range (0 to size-1), preventing both negative index attacks (accessing before the array) and positive out-of-bounds access (accessing past the array end). Using the array size constant in the check ensures the validation stays correct if the array size changes. Returning early on invalid input (fail-fast) prevents execution from continuing with unsafe values.
Proper loop bounds to avoid off-by-one errors
#include <string.h>
void safe_loop() {
char buffer[10];
// Correct: i < 10, not <= 10
// Valid indices are 0-9, so loop condition is i < 10
for (int i = 0; i < 10; i++) {
buffer[i] = 'A';
}
buffer[9] = '\0'; // Explicit null termination at last position
}
void safe_copy_loop(const char *src) {
char dest[100];
int i;
// Safe: check both src termination AND dest capacity
for (i = 0; i < 99 && src[i] != '\0'; i++) {
dest[i] = src[i];
}
dest[i] = '\0'; // Null terminate (i is at most 99)
// Both conditions prevent overflow:
// - i < 99 ensures we don't write past dest[99]
// - src[i] != '\0' stops at end of source string
}
Why this works: Using i < size instead of i <= size in loop conditions prevents off-by-one errors that access one element past the array end (e.g., buffer[10] when valid indices are 0-9). The dual condition in the copy loop (i < 99 && src[i] != '\0') provides two layers of protection: i < 99 prevents writing past the destination buffer (leaving room for the null terminator at position 99), while src[i] != '\0' ensures we stop at the source string's end. Explicit null termination after the loop ensures the destination string is properly terminated.
Allocating proper buffer size with null terminator
#include <string.h>
#include <stdlib.h>
char* safe_string_allocation(const char *name) {
if (name == NULL) {
return NULL;
}
size_t len = strlen(name);
// Correct: allocate length + 1 for null terminator
char *buffer = (char *)malloc(len + 1);
if (buffer == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return NULL;
}
// Safe: buffer has exactly the right size
strcpy(buffer, name); // Copies len + 1 bytes (including '\0')
return buffer;
}
void safe_strncpy_usage(const char *input) {
char buffer[10];
// Use strncpy with proper size
strncpy(buffer, input, sizeof(buffer) - 1);
// CRITICAL: Always null-terminate when using strncpy
buffer[sizeof(buffer) - 1] = '\0';
// Now safe to use buffer as a string
printf("%s\n", buffer);
}
Why this works: Allocating strlen(name) + 1 bytes accounts for both the string content and the required null terminator, preventing heap overflow when strcpy() writes the null byte. Using sizeof(buffer) - 1 as the length parameter to strncpy() reserves the last byte for explicit null termination, because strncpy() only adds a null terminator if the source string fits entirely within the specified length. The explicit buffer[sizeof(buffer) - 1] = '\0' ensures null termination in all cases, preventing reads past the buffer end when the string is used later. Checking malloc() return value prevents null pointer dereference if allocation fails.
Using C++ safe containers with bounds checking
#include <string>
#include <vector>
#include <stdexcept>
#include <iostream>
void process_input(const std::string& user_input) {
// Safe: std::string handles memory automatically, no fixed size limit
std::string buffer = user_input;
// Safe access with bounds checking
if (!buffer.empty()) {
char first = buffer.at(0); // at() throws on out-of-bounds
std::cout << "First character: " << first << std::endl;
}
// operator[] doesn't check bounds (for performance)
// Use at() when bounds are uncertain
}
void safe_vector_access(int user_index) {
std::vector<int> data(10);
// Safe: at() performs runtime bounds checking
try {
data.at(user_index) = 42;
std::cout << "Successfully set data[" << user_index << "]" << std::endl;
} catch (const std::out_of_range& e) {
// Exception thrown if user_index < 0 or >= 10
std::cerr << "Index out of range: " << e.what() << std::endl;
}
}
void manual_validation(int user_index) {
std::vector<int> data(10);
// Alternative: validate manually for better error messages
if (user_index >= 0 && user_index < data.size()) {
data[user_index] = 42; // operator[] for performance (no checking)
} else {
std::cerr << "Invalid index " << user_index
<< " (valid: 0-" << data.size() - 1 << ")" << std::endl;
}
}
Why this works: std::string and std::vector automatically manage memory allocation and deallocation, eliminating manual buffer management errors. The at() method performs runtime bounds checking and throws std::out_of_range exception on invalid access, preventing silent memory corruption. Unlike C arrays, these containers know their own size (.size()), enabling accurate validation. std::string grows automatically as needed, eliminating fixed-size buffer overflows. Using try-catch or manual validation (index < data.size()) provides explicit error handling instead of undefined behavior.