CWE-121: Stack-based Buffer Overflow
Overview
A stack-based buffer overflow happens when a program writes more data into a stack-allocated buffer (a local variable, or occasionally a function parameter) than the buffer can hold, corrupting whatever the compiler placed adjacent to it - other local variables, saved registers, and the function's saved return address. Because it clobbers control-flow data the CPU trusts implicitly, it is one of the most exploited vulnerability classes in native software. This is a manual-memory-management weakness: it requires a language that performs no automatic bounds checking on ordinary buffer writes, so it is effectively specific to C and C++.
Relationship to Other CWEs
CWE-121 is the stack-allocation-specific variant of the broader CWE-787 (Out-of-bounds Write) - the same missing-bounds-check root cause, but the corrupted memory happens to be on the stack rather than the heap. That distinction matters for exploitation and defense: a stack overflow can directly overwrite a saved return address, which is why stack canaries and -fstack-protector exist specifically for this case, whereas a heap overflow (CWE-122, which has no page here) instead corrupts heap metadata or adjacent heap objects and is mitigated differently.
Risk
Critical: A stack buffer overflow that reaches the saved return address gives an attacker control of where the function returns to, enabling arbitrary code execution. Even overflows that don't reach the return address can corrupt adjacent local variables (including security-relevant ones, such as a flag checked later in the same function) or crash the process.
Remediation Steps
Core Principle: Never write more into a stack buffer than it was allocated to hold; prefer types and functions that enforce this automatically over manually tracking the size yourself.
Trace the Data Path
- Source: Any length or content value influenced by user input, file data, or network data that ends up copied into a fixed-size stack buffer
- Sink: The copy or write into the stack buffer - an unbounded copy/format function, or a manual loop indexing into it
- Missing Controls: No check that the incoming data's length fits within the destination buffer's actual declared size before the write happens
Use Bounds-Checked Copy and Format Functions (Primary Defense)
Replace copy and formatting functions that have no awareness of the destination's capacity with ones that take an explicit size limit, and check whether the result was truncated rather than assuming it fit. Prefer a dynamically-sized, self-managing string/buffer type over a fixed-size stack array wherever the maximum size isn't small and truly fixed.
Validate Length Before Every Copy (Defense in Depth)
- Check
input_length < buffer_sizebefore copying, reserving room for a terminator where the data is a string - Reject oversized input with an explicit error rather than silently truncating it
- Use the actual declared buffer size (via a
sizeof-style mechanism) in the check, never a hardcoded number that can drift out of sync with the buffer's real declaration
Harden the Runtime as Defense in Depth
Enable stack canaries, address space layout randomization, and non-executable stack protections so that an overflow that does happen is harder to turn into code execution. These reduce exploitability; they do not prevent the underlying corruption, so they are not a substitute for fixing the write itself.
Test with Overflow Payloads
- Input exactly at the buffer's capacity, one byte over, and far over
- Fuzz any function that copies untrusted data into a fixed-size buffer
- Confirm with a sanitizer that no out-of-bounds write occurs, not just that the program didn't crash
- Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
// VULNERABLE - pseudo-code
function handle_input(user_input):
buffer = allocate_on_stack(fixed_size)
copy_into(buffer, user_input) // no check that user_input fits in fixed_size
// Attack: user_input longer than fixed_size
// Result: the copy writes past buffer, corrupting adjacent stack memory including
// the saved return address
Why this is vulnerable: nothing in the copy expression knows how big buffer is. The length that governs the write comes entirely from user_input, and the destination's capacity is a fact the surrounding code holds but the copy never consults. That is why the shape survives review: the line reads as copying a value, not as writing an attacker-chosen number of bytes.
Past the end of a stack buffer sit the other locals, the saved frame pointer and the return address, in an order the compiler chose and the source never states. The write itself succeeds and nothing observable happens at the copy site - the consequence arrives later, at the function's return. That distance between cause and symptom is why a stack overflow usually reproduces as a crash inside a function that has already finished, or as a local variable whose value changed for no reason the source can explain.
Secure Patterns
// SECURE - pseudo-code
function handle_input(user_input):
buffer = allocate_on_stack(fixed_size)
if length(user_input) >= fixed_size:
reject("input too large for destination")
copy_into(buffer, user_input)
Why this works: Validating the incoming length against the stack buffer's actual capacity before the copy happens means oversized input is refused instead of silently overflowing into adjacent stack memory. A copy function that enforces a destination size limit internally (rather than a hand-written check like this one) is preferable, since it can't be bypassed by a call site that forgets to add the check.
Common Pitfalls
- Swapping in a "safer" function without fixing the size argument: Replacing an unbounded copy with a length-limited one but still sizing the copy from the source data (or an unrelated constant) instead of the destination buffer's actual capacity leaves the same overflow possible - the function name changed, the bound didn't.
- An off-by-one in the new bounds check: Adding a length check that uses
<=instead of<, or that forgets to reserve a byte for a string terminator, still permits exactly the one byte that overflows the buffer - the check exists but doesn't cover the actual valid range. - Treating compiler mitigations as the fix: Stack canaries, ASLR, and DEP make an overflow harder to exploit, but they act after the corruption has already happened (a canary check only runs at function return) and do nothing to prevent the write - the overflow itself must still be eliminated at the copy site.
- Validating length once at an outer boundary: A check performed where data first enters the program (an HTTP handler, a parser's entry point) doesn't automatically cover a second copy or transformation step deeper in the call stack that reuses the same or a different fixed-size buffer without repeating the check.
Language-Specific Guidance
- C - safe string/copy functions (
fgets,snprintf,strlcpy/strlcat), explicit length validation, thestrncpyandstrncatpitfalls - C++ -
std::string/std::array, checked.at()access, why C++ inherits the same risk when raw arrays and C string functions are used