CWE-787: Out-of-bounds Write - C++
Overview
C++ inherits C's raw arrays and pointer arithmetic, so the same out-of-bounds write bugs are possible - operator[] on std::vector and std::array performs no bounds checking by default, and new[]/manual indexing carries all the same risk as C arrays. The fix in C++ is usually to stop managing size and capacity by hand, so that an out-of-bounds write takes a deliberate step around a checked API rather than a miscounted index.
Primary Defence: Use std::vector/std::string/std::array instead of raw arrays and new[], and use .at() (which throws std::out_of_range) instead of operator[] whenever the index isn't already provably in range. For non-owning views into contiguous memory, use std::span (C++20) instead of a raw pointer and separate length.
Common Vulnerable Patterns
Raw Array with Unchecked Index
// VULNERABLE - no bounds checking on operator[]
void process(int *buffer, size_t bufferSize, size_t index, int value) {
buffer[index] = value; // no check that index < bufferSize
}
Why this is vulnerable: operator[] on a raw pointer performs pointer arithmetic with no bounds check. If index is attacker-influenced and exceeds bufferSize, the write lands outside the allocated block.
std::vector with Unchecked operator[]
// VULNERABLE - operator[] does not bounds-check, even on std::vector
void updateScore(std::vector<int>& scores, size_t playerIndex, int newScore) {
scores[playerIndex] = newScore; // out-of-bounds write if playerIndex >= scores.size()
}
Why this is vulnerable: std::vector::operator[] is specified to have undefined behavior when the index is out of range - it does not throw or check, matching raw-array semantics for performance, so an unvalidated index carries the same risk here as on a raw array.
Manual Buffer Growth
// VULNERABLE - new size calculated by hand, copy can overrun the new buffer
void append(char *&buffer, size_t &size, const char *extra, size_t extraLen) {
char *bigger = new char[size + extraLen]; // no overflow check on size + extraLen
memcpy(bigger, buffer, size);
memcpy(bigger + size, extra, extraLen); // writes past `bigger` if the size math was wrong
delete[] buffer;
buffer = bigger;
size += extraLen;
}
Why this is vulnerable: Every part of this - the size addition, the allocation, and the two copies - has to stay consistent by hand. A miscalculated size, an integer overflow in size + extraLen, or a copy length that doesn't match the allocation all produce an out-of-bounds write, and there is no structural guarantee any of them are correct.
Secure Patterns
Checked Access with .at()
#include <vector>
#include <stdexcept>
#include <iostream>
void updateScore(std::vector<int>& scores, size_t playerIndex, int newScore) {
try {
scores.at(playerIndex) = newScore; // throws std::out_of_range if playerIndex is invalid
} catch (const std::out_of_range& e) {
std::cerr << "Invalid player index: " << playerIndex << " (" << e.what() << ")\n";
}
}
Why this works: .at() performs the bounds check operator[] skips, and throws instead of writing out of bounds. The cost is a check on every access - use operator[] only where the index has already been validated or is structurally guaranteed in range (e.g. a loop bound by .size()).
std::vector Manages Its Own Growth
#include <vector>
void append(std::vector<char>& buffer, const char* extra, size_t extraLen) {
// insert grows the underlying storage itself; no manual size math, no manual copy
buffer.insert(buffer.end(), extra, extra + extraLen);
}
Why this works: std::vector owns its capacity and growth logic internally, so there's no size calculation, allocation, or copy for calling code to get wrong. The only way to write out of bounds here would be to call operator[]/.data() with an index or offset that isn't validated - the growth path itself is not where the risk lives anymore.
std::span for Bounds-Aware Views (C++20)
#include <span>
#include <stdexcept>
void writeAt(std::span<int> buffer, size_t index, int value) {
if (index >= buffer.size()) {
throw std::out_of_range("index out of range for span");
}
buffer[index] = value;
}
Why this works: std::span carries its length alongside the pointer, so a function that takes a span instead of a separate (pointer, length) pair can't have the two get out of sync - the size used for the bounds check is always the size of the actual data being pointed to. This replaces the C pattern of passing a raw pointer and trusting the caller to pass a matching, correct length.
Testing
- Compile with AddressSanitizer and UndefinedBehaviorSanitizer (
-fsanitize=address,undefined -g -O1) and run the test suite 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++ (bounds checks onoperator[]and iterator access are in thefastcategory;extensiveadds unrelated checks on top and works too, but isn't required for this one), 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.