CWE-125: Out-of-bounds Read - C++
Overview
C++ inherits C's raw arrays and pointer arithmetic, so the same out-of-bounds reads are possible - operator[] on std::vector, std::array, and std::string performs no bounds checking by default. The fix in C++ is usually to stop indexing by hand: use the bounds-checked .at() accessor, prefer range-based iteration that never computes an index at all, and use std::span (C++20) to keep a pointer and its length bound together instead of passing them as two separate parameters that can drift out of sync.
Primary Defence: Use .at() instead of operator[] whenever the index isn't already provably in range, and prefer std::span/iterators over a raw (pointer, length) pair so the length travels with the data instead of being trusted separately.
Common Vulnerable Patterns
Unchecked operator[]
// VULNERABLE - operator[] does not bounds-check, even on std::vector
int readScore(const std::vector<int>& scores, size_t index) {
return scores[index]; // undefined behavior if index >= scores.size()
}
Why this is vulnerable: std::vector::operator[] is specified to have undefined behavior on an out-of-range index - it does not throw or check, matching raw-array performance. An attacker-influenced index reads whatever memory follows the vector's internal buffer.
Raw Pointer and Length Passed Separately
// VULNERABLE - length is trusted independently of what buffer actually points to
void readPayload(const char *buffer, size_t bufferSize, size_t requestedLength) {
for (size_t i = 0; i < requestedLength; i++) { // no check against bufferSize
process(buffer[i]);
}
}
Why this is vulnerable: Nothing ties requestedLength to bufferSize - if a caller (or the data driving requestedLength) supplies a value larger than the buffer actually holds, the loop reads past it.
Iterating with a Miscalculated End Index
// VULNERABLE - off-by-one: valid indices are 0..size-1, but the loop reads data[size]
void sumBuffer(const int *data, size_t size) {
int total = 0;
for (size_t i = 0; i <= size; i++) {
total += data[i];
}
}
Why this is vulnerable: The <= comparison includes data[size], one element past the array - the same off-by-one mistake that occurs in C, since C++ doesn't add automatic bounds checking to raw pointer indexing.
Secure Patterns
Checked Access with .at()
#include <vector>
#include <stdexcept>
#include <iostream>
int readScore(const std::vector<int>& scores, size_t index) {
try {
return scores.at(index); // throws std::out_of_range if index is invalid
} catch (const std::out_of_range& e) {
std::cerr << "Invalid score index: " << index << " (" << e.what() << ")\n";
return -1;
}
}
Why this works: .at() performs the bounds check operator[] skips and throws instead of reading out of bounds. Use operator[] only where the index has already been validated or is structurally guaranteed in range (e.g. a loop bound by .size()).
std::span Ties Pointer and Length Together (C++20)
#include <span>
#include <stdexcept>
void readPayload(std::span<const char> buffer, size_t requestedLength) {
if (requestedLength > buffer.size()) {
throw std::out_of_range("requested length exceeds buffer size");
}
for (size_t i = 0; i < requestedLength; i++) { // safe: checked against buffer.size()
process(buffer[i]);
}
}
Why this works: std::span carries its length alongside the pointer, so the function can validate requestedLength against the actual size of the data it was given rather than trusting a second, independently-supplied size parameter that could disagree with it.
Range-Based Iteration (No Index at All)
#include <vector>
void sumBuffer(const std::vector<int>& data) {
int total = 0;
for (const auto& value : data) { // no index arithmetic, no off-by-one possible
total += value;
}
}
Why this works: A range-based for loop never computes an index, so there's no boundary calculation to get wrong. Use it whenever the loop doesn't need the index itself.
Testing
- Compile with AddressSanitizer and UndefinedBehaviorSanitizer (
-fsanitize=address,undefined -g -O1) and run with normal, boundary, and oversized/negative indices. - Build and test with the standard library's hardened mode, which adds bounds checks to
operator[]and the other normally-unchecked operations:-D_GLIBCXX_ASSERTIONSon libstdc++,-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FASTon libc++ (thefastcategory coversoperator[]and iterator access;extensiveadds unrelated checks on top and works too, but isn't required here), and on the Microsoft STL the equivalent container checks are already on in debug builds, where_ITERATOR_DEBUG_LEVELdefaults to 2. These turn an out-of-bounds subscript into a deterministic abort rather than whatever the adjacent memory happened to hold, which is what makes a failing test reproducible. - Run under Valgrind as an independent check.
Common Pitfalls
- Using
.at()inconsistently: Switching the top-level accessor to.at()but leaving a helper function nearby (or an internal loop within the same class) still indexing the same container withoperator[]re-opens the same unchecked read - the fix has to reach every access point, not just the one the finding pointed at. - Validating
requestedLengthagainst a stale or assumed size instead ofspan.size()/vector.size(): A separately-tracked size variable can drift from the container's actual size after a resize, a move, or a reallocation, and the check then passes a length that reads past the end. - Assuming
std::string/std::vectorare always safe because they're not raw arrays: Both still use uncheckedoperator[]by default - the safety comes specifically from using.at()or iterators, not from the container type alone.