Skip to content

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::array instead of raw arrays/pointers
  • Safe C functions: Replace strcpy with strncpy, sprintf with snprintf, strcat with strncat
  • 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_size before access
  • Validate untrusted indices explicitly: If index comes from untrusted data, validate before use
  • Use unsigned types: Use size_t or unsigned int for 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 + 1 for the \0 terminator
  • 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_size before 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-strong to 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.

Additional Resources