Skip to content

CWE-125: Out-of-bounds Read

Overview

Out-of-bounds read happens when a program reads data past the end, or before the beginning, of the buffer or array it is reading from - returning whatever adjacent memory happens to hold instead of failing safely. It typically results from a missing bounds check, an off-by-one loop condition, or code that trusts a length or offset value from untrusted input without validating it against the buffer's real size. This is almost exclusively a manual-memory-management problem: languages with automatic bounds checking on normal indexing (Java, C#, Python, safe Rust) throw an exception instead of reading adjacent memory.

Relationship to Other CWEs

CWE-125 is itself Allowed for mapping, so there is no need to reach for its variants - over-read CWE-126, under-read CWE-127 - unless the finding turns on which end of the buffer was passed.

Risk

High: Out-of-bounds reads can expose passwords, keys, and tokens from adjacent memory, crash the application, or leak stack and heap addresses that defeat ASLR and open the way to a follow-on exploit. Heartbleed (CVE-2014-0160) was an out-of-bounds read that disclosed server memory.

Remediation Steps

Core Principle: Never read past the bounds of the buffer that was actually allocated; validate both the lower and upper bound before every read that uses an untrusted index, offset, or length.

Trace the Data Path

  • Source: Any index, offset, or length value influenced by user input, file data, network data, or an attacker-controlled calculation
  • Sink: The raw memory read - an array index access, pointer dereference, or a copy function reading from a source with an attacker-influenced offset or length
  • Missing Controls: No check that the index/offset stays within [0, size), or a length taken from input without validating it against the buffer's actual size

Validate Every Read Against the Buffer's Real Size (Primary Defense)

  • Check index >= 0 && index < size before every read
  • For a range read, confirm offset <= buffer_size first, then reject the read when length > buffer_size - offset. Do not write offset + length <= buffer_size: the addition happens before the comparison, so a large enough offset wraps the sum back into range and the check passes
  • Never trust a length or offset field taken directly from user input, a file, or the network - validate it against the buffer's real allocated or received size, not just against the sender's claimed size

Prefer Bounds-Checked Abstractions (Defense in Depth)

Where a language or library offers both a checked and an unchecked way to read from a buffer, default to the checked one, and only drop to the unchecked form with an explicit, verified bounds check immediately before it.

Harden the Runtime as Defense in Depth

  • Enable compiler protections and runtime bounds-checking modes during development and testing
  • Run sanitizers and fuzzers in development and CI so out-of-bounds reads are caught before release

Test with Malicious Inputs

  • Oversized length and offset values, including values crafted to overflow a size calculation
  • Negative or very large index values where the code assumes a small positive number
  • Exact boundary values (size - 1, size, size + 1)
  • Re-scan with the security scanner to confirm the finding is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
buffer = allocate(fixed_size)
value = buffer[index]        // no check that index is within [0, fixed_size)
// Attack: index is taken from untrusted input and exceeds fixed_size, or is negative
// Result: the read returns data from outside buffer, disclosing adjacent memory

Why this is vulnerable: the read succeeds. Nothing faults, nothing throws, and there is no return code to check - value receives whatever bytes sit at that address and the program carries on using them as if they were its own. That silence is why the weakness survives review and testing: the wrong answer looks exactly like the right one, and only the contents of the adjacent memory on that run separate them.

index in the sketch stands for three values that a scanner may report separately and that fail identically here: an array index, an offset added to a base pointer, and a length or count driving a copy or a loop. The third is the one most often missed, because that code usually does have a bounds check - it just validates the length against the size the sender claimed rather than against the bytes actually received. Whichever form the finding takes, the value has to be checked against the buffer's real allocated size, and against both ends of it.

Secure Patterns

// SECURE - pseudo-code
buffer = allocate(fixed_size)
if index < 0 or index >= fixed_size:
    reject("index out of range")
value = buffer[index]

Why this works: The index is checked against the buffer's real size, in both directions, before the read happens, so an out-of-range value is rejected instead of silently returning data from outside the buffer. A bounds-checked container or accessor that does this automatically is better than a hand-written check because no call site can forget it.

Common Pitfalls

  • Checking only the upper bound: Adding if index < size stops reads past the end, but whether it stops a negative index depends on the types involved - where the index and the bound are both signed (an int length, or a bare numeric literal), the negative passes that single check and reads before the buffer starts. Write both bounds explicitly rather than depending on a conversion rule to cover one of them; the C page has the specific case.
  • Validating a claimed length against another claimed value: Checking a length field from a packet or file against a second field from the same untrusted source, rather than the buffer's actual allocated or received size, doesn't verify anything - both numbers are attacker-controlled.
  • Adding the bounds check after the read, or in a different function than the one performing it: A check that runs after the memory access, or that validates the index in a caller while a callee performs the actual indexed read without re-checking, doesn't prevent the out-of-bounds access - it only detects it after the fact, if at all.
  • Trusting offset + length <= buffer_size as written: This form performs the addition before the check runs, so a large enough offset can wrap the sum around and pass the check while the actual read is nowhere near the buffer. Confirming offset <= buffer_size and then rearranging to length > buffer_size - offset avoids the overflow instead of just moving it.

Language-Specific Guidance

  • C - explicit bounds validation, memcpy/length-field handling, AddressSanitizer and compiler hardening flags
  • C++ - .at(), std::span, avoiding raw pointer arithmetic and unchecked operator[]

Additional Resources