Skip to content

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.

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 0 through N-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 exactly length when the data being copied needs length + 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, not index <= 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, not index <= 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 0 and size 1 collections
  • 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 exactly count elements 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 reserve size - 1 bytes 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 < size logic as any other index, not assumed correct because it came bundled with the payload.

Additional Resources