CWE-121: Stack-based Buffer Overflow - C++
Overview
C++ inherits C's raw arrays and C string functions, so a fixed-size stack array (char buffer[64]), or a local std::array accessed through unchecked operator[], carries exactly the same stack-overflow risk as C. The difference is that C++ also offers types that manage their own size and either grow automatically or check bounds on request - the fix is usually to stop using fixed-size stack buffers and manual indexing for anything whose size depends on input, rather than to write a more careful bounds check by hand.
Which container puts the write on the stack matters for this CWE. std::array stores its elements inline, so a local one is a stack buffer and overflowing it is CWE-121. std::vector and a grown std::string hold their elements in a separate heap allocation - only the small handle sits on the stack - so an out-of-range operator[] on either is undefined behavior somewhere other than this frame. For a vector, how far out of range decides what it reaches: at or beyond capacity() the write lands outside the allocation, which is CWE-787 (Out-of-bounds Write) and specifically the heap variant CWE-122; an index between size() and capacity() is still out of bounds for the container, but stays inside the storage it reserved. A string does not divide the same way, so do not carry the capacity() comparison over to one: every index past size() is undefined, while s[s.size()] is valid and refers to the terminator, which may be read and may be assigned '\0'. The missing bounds check is identical in every one of these cases and so is the fix; what differs is what gets corrupted, which decides whether stack canaries are relevant and what a crash report will look like.
Primary Defence: Use std::string for text and std::vector/std::array for other data instead of a raw stack array, and use .at() (which throws std::out_of_range) instead of operator[] whenever the index isn't already provably in range.
Common Vulnerable Patterns
Fixed-Size Stack Buffer with a C String Function
#include <cstring>
void copyUserData(const char* userInput) {
char buffer[64]; // stack-allocated, fixed size
// VULNERABLE - strcpy has no awareness of buffer's 64-byte capacity
strcpy(buffer, userInput);
}
Why this is vulnerable: Declaring buffer as a C++ local variable doesn't change how strcpy() behaves - it's the same function from the C standard library, with the same lack of a size parameter. Input longer than 63 characters overflows into whatever the compiler placed after buffer on the stack, including a saved return address.
Unchecked operator[] on a Fixed-Size Container
#include <array>
void updateScore(std::array<int, 10>& scores, size_t index, int newScore) {
// VULNERABLE - operator[] does not bounds-check, even on std::array
scores[index] = newScore;
}
Why this is vulnerable: std::array::operator[] is specified to have undefined behavior on an out-of-range index - it matches raw-array performance characteristics by design, not safety ones. std::array stores its elements inline rather than in a separate allocation, so when the caller's scores is a local the write corrupts the same kind of adjacent stack memory a raw C array would. The function itself cannot tell: it takes a reference, and the same code writes into the heap when the caller's array is a member of a heap-allocated object.
Secure Patterns
std::string Instead of a Fixed-Size Stack Buffer
#include <string>
#include <stdexcept>
#include <iostream>
void copyUserData(const std::string& userInput) {
// Optional: enforce a genuine upper bound by rejecting, not truncating
constexpr size_t maxLen = 64;
if (userInput.length() > maxLen) {
throw std::length_error("input exceeds the permitted length");
}
// Safe: std::string manages its own memory and grows as needed
std::string buffer = userInput;
std::cout << "User: " << buffer << std::endl;
}
Why this works: std::string allocates however much memory the actual content needs and reallocates automatically when it grows - there is no fixed-size stack buffer for a copy to overflow. Where an upper bound genuinely exists, reject the oversized value rather than calling resize() on it: truncation does not refuse the input, it silently substitutes a different one, so a username that is too long becomes a valid-looking username belonging to somebody else, and a UTF-8 value can be cut mid-sequence. The C page's length check follows the same rule.
Checked Access with .at()
#include <array>
#include <stdexcept>
#include <iostream>
bool updateScore(std::array<int, 10>& scores, size_t index, int newScore) {
try {
scores.at(index) = newScore; // throws std::out_of_range if index is invalid
return true;
} catch (const std::out_of_range& e) {
std::cerr << "Invalid index: " << index << " (" << e.what() << ")\n";
return false; // report the failure - the write did not happen
}
}
Why this works: .at() performs the bounds check operator[] skips and throws instead of writing out of bounds. Use operator[] only where the index has already been validated or is structurally guaranteed to be in range (for example, a loop bounded by .size()); use .at() everywhere else.
The return value is part of the fix, not decoration. A void version that logs and returns leaves the caller believing the score was recorded, so the memory-safety bug is traded for a silent data-loss bug that the log line is the only evidence of. Either report the failure as this one does, or let the exception propagate to a caller that can decide - catching it at the point of the write is only worth doing if something there can act on it.
Testing
- Compile with AddressSanitizer and UndefinedBehaviorSanitizer (
-fsanitize=address,undefined -g -O1) and run with normal, boundary, and oversized/negative indices. - Where the standard library implementation supports it, build and test with
_GLIBCXX_ASSERTIONS(libstdc++) or the equivalent hardened mode, which adds bounds checks to normally-unchecked operations likeoperator[]in debug/test builds. - Confirm
.at()calls actually throw (and are caught) for out-of-range indices in a unit test, rather than assuming the change is correct by inspection.
Common Pitfalls
- Assuming a
std::array/std::vectoris automatically safe: Moving from a raw C array tostd::arrayorstd::vectordoesn't add bounds checking by itself -operator[]on either type is still unchecked undefined behavior on an out-of-range index. The safety comes from switching to.at()or validating the index, not from the container type alone. - Mixing C++ containers with a C API that still expects a raw buffer: Calling
.data()on astd::string/std::vectorto pass it to a C function, then having that function write back into the buffer past.size(), reintroduces exactly the overflow the C++ container was meant to prevent - the container has no way to know about writes that happen through a raw pointer it handed out. - Wrapping
operator[]in a try/catch "just in case":operator[]does not throw on an out-of-range access (that's.at()'s behavior) - wrapping anoperator[]call in atry/catchblock catches nothing, because the undefined behavior has already happened by the time any exception machinery could run.