CWE-193: Off-by-one Error
Overview
An off-by-one error happens when code calculates or checks a maximum or minimum boundary that is exactly one position wrong - a loop that runs one iteration too many (<= where < belongs), a bounds check that allows an index equal to the array's size, or an allocation that is one byte short of what a subsequent write actually needs. The underlying mistake is almost always confusing an array's size (how many elements it holds) with its highest valid index (size minus one).
Relationship to Other CWEs
MITRE marks this weakness as not restricted to any one language: the boundary-miscounting mistake happens in any language with indexable collections, though the consequence differs. In C/C++ an off-by-one read or write touches memory outside the buffer, while in a bounds-checked language such as Java, Python or C# the same mistake typically throws an exception or silently returns a wrong result rather than corrupting memory.
- CWE-193 (this page) - a boundary check or loop bound that exists but is wrong by exactly one
- CWE-682 (Incorrect Calculation) - the Pillar this Base-level page sits under. No page here
- CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer) - what an off-by-one CanPrecede once it drives a read or a write, and the parent of the out-of-bounds pages CWE-125 and CWE-787
- CWE-170 (Improper Null Termination) - another
CanPrecede, and itself a common special case of off-by-one: the
+1a null terminator needs is forgotten - CWE-617 (Reachable Assertion) - the third CanPrecede, where the miscounted boundary trips a bounds assertion rather than passing it. No page here
- CWE-129 (Improper Validation of Array Index) - related but distinct: CWE-129 is a missing check on an untrusted index, while this page is a check that exists and is wrong by exactly one
- CWE-135 (Incorrect Calculation of Multi-Byte String Length) - the sibling under the same pillar where the count is wrong by a whole encoding rather than by one, because a length measured in bytes was used as a count of characters. MITRE records no direct relationship between the two
Risk
Medium-High: In C/C++, an off-by-one write past the end of a buffer corrupts adjacent memory - often the byte right after a fixed-size array, which is exactly where an off-by-one string-copy bug lands, making it a common path to code execution. An off-by-one read discloses one element of adjacent memory. In managed languages, the same boundary mistake generally raises an index-out-of-range exception instead of corrupting memory, but an unhandled exception is still a denial-of-service, and a boundary mistake in a check (rather than a raw access) can silently accept or reject the wrong input without ever throwing.
Remediation Steps
Core Principle: An array or buffer of size N has valid indices
0throughN-1- every loop bound and boundary check must use<, never<=, against the size, and every allocation that will hold a terminator or sentinel value must add one element for it.
Trace the Data Path
- Source: Any size, length, or count value that drives a loop bound, an allocation, or a boundary check - especially one derived from
strlen(), a collection's length, or a value read from input - Sink: The loop or indexed access itself, or the allocation that a subsequent write depends on being large enough
- Missing Controls: A
<=comparison against a size where<belongs, an allocation sized to exactlylengthwhen the data being copied needslength + 1, or a bounds check that was correct when written and was not updated after a related change
Use Correct Loop and Boundary Comparisons (Primary Defense)
- Iterate with
index < size, notindex <= size- the valid range for a size-N collection is[0, size) - When checking whether an index is in range, use
index >= 0 && index < size, notindex <= size - For any allocation or buffer that will hold a null terminator, sentinel, or extra delimiter, size it as
data_length + 1(or however many extra positions the terminator/sentinel needs) - see CWE-170 for the null-terminator case in depth
Prefer Bounds-Checked Collections and Iteration (Defense in Depth)
- Use range-based/foreach iteration or a language's built-in length-tracking collection type where available, rather than manually computed loop bounds - it removes the off-by-one calculation entirely
- Where a manual index is unavoidable, prefer a checked-access method (one that throws or refuses on an out-of-range index) over an unchecked one, and reserve the unchecked form for code paths where the index has already been proven in range
- For reverse iteration over an unsigned counter, decrement inside the loop condition rather than comparing against
>= 0, which is always true for an unsigned type and produces a wraparound bug related to CWE-191 rather than a simple off-by-one
Test with Boundary Values
- Size
0and size1collections - The last valid index (
size - 1) and the first invalid one (size) - A string or buffer exactly at the allocated capacity, and one character over it
- Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
// VULNERABLE - pseudo-code
function init_array(array, size):
for i in 0 to size inclusive: // BUG: should stop before size
array[i] = 0 // writes one element past the end when i == size
function is_valid_index(index, size):
return index <= size // BUG: size itself is out of range
Why this is vulnerable: both bugs are a single character, and both are in the direction that admits one extra element rather than rejecting one valid element. That asymmetry is why they survive: an off-by-one the other way is found immediately, because a legitimate input fails and someone reports it, whereas this one only misbehaves on the boundary value and every input below it behaves correctly.
What that one element costs depends on what follows the resource: a heap allocation is followed by the allocator's own bookkeeping, and a stack array by the other locals and the saved return address. The practical distance between "writes one byte too many" and "controls where the function returns to" is small. The habit that prevents the second example is stating the rule rather than the comparison: for a resource of size elements the valid indices are 0 to size - 1, so the boundary check is index < size and the loop condition is the same expression, not one adapted from it.
Secure Patterns
// SECURE - pseudo-code
function init_array(array, size):
for i in 0 to size exclusive: // stops at size - 1
array[i] = 0
function is_valid_index(index, size):
return index >= 0 and index < size // size itself correctly excluded
Why this works: Using an exclusive upper bound (< size) instead of an inclusive one (<= size) matches how indexable collections are actually addressed - valid positions run from 0 up to, but not including, the size. The same rule applies whether the boundary is a loop condition or a validation check.
Common Pitfalls
- Fixing the flagged comparison without checking sibling code: A scanner or review typically flags one
<=that should be<, but the same collection is often indexed the same wrong way elsewhere in the same function - fixing only the reported line leaves the pattern exploitable nearby. - Reserving space for data but not for its terminator or sentinel: Allocating exactly
strlen(src)bytes for a C string, or exactlycountelements for a buffer that a caller will null-terminate or delimit, is one element short - the write that adds the terminator lands out of bounds. - Assuming
strncpy-style bounded copies guarantee termination: A bounded copy function that stops at the destination's size does not add a terminator if the source was at least that long - the caller still has to reservesize - 1bytes for data and explicitly set the last byte, or the "safe" copy leaves an unterminated buffer. - Trusting a length field that travels with the data it describes: A size or count embedded in a network message or file format is still attacker-controlled, not a validated bound - it must be checked with the same
< sizelogic as any other index, not assumed correct because it came bundled with the payload.