Back to Cases
HighCWE-787

CBC Encrypt Out-of-Bounds Read/Write on Sub-Block Length

tiny-AES-c2026-02-23CWE-787High

CVSS v4.0

Score: 7.7 (High) Vector: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:H/SC:N/SI:N/SA:N

  • AV:N — The library is commonly used in network-facing applications processing encrypted protocol data.
  • AC:L — No special conditions required beyond calling the function with a sub-block length.
  • AT:P — The attacker needs the calling application to pass sub-block-length data to the encrypt function, which requires the application to handle variable-length inputs without pre-padding.
  • PR:N — No privileges required; the attacker influences input data length through normal application interaction.
  • UI:N — No user interaction needed.
  • VC:L — Up to 15 bytes of adjacent memory are leaked through the encrypted output (information disclosure).
  • VI:H — Up to 15 bytes of adjacent memory are corrupted (XORed with IV and AES-encrypted in-place), destroying integrity of adjacent data structures.
  • VA:H — Reliable crash/denial of service when heap protections detect corruption; silent corruption in unprotected environments.

CWE

CWE-787: Out-of-bounds Write

Affected Files

  • aes.c:505 — Loop condition for (i = 0; i < length; i += AES_BLOCKLEN) enters the loop body for any length > 0
  • aes.c:507-508XorWithIv and Cipher process a full 16-byte block regardless of actual buffer size

Description

When AES_CBC_encrypt_buffer() is called with a length value between 1 and 15 (inclusive), the loop condition i < length evaluates to true (since i starts at 0 and 0 < length for any positive length). The function then processes a full 16-byte block through XorWithIv() and Cipher(), reading and writing 16 bytes from a buffer that may be as small as 1 byte.

This is a specific and particularly dangerous case of the non-block-aligned length vulnerability. Unlike the general case where the overflow is at the end of a multi-block message, here the very first (and only) block operation overflows from the start. If the caller provides a buffer of length bytes:

  • XorWithIv() at line 507 reads and writes bytes buf[0] through buf[15], accessing 16 - length bytes beyond the buffer
  • Cipher() at line 508 reads and writes the full 16-byte state_t, corrupting the same out-of-bounds region
  • memcpy(ctx->Iv, Iv, AES_BLOCKLEN) at line 513 may copy from the corrupted buffer region

For a caller with a 1-byte buffer, 15 bytes of adjacent memory are read, encrypted, and written back — a combined read and write overflow of 15 bytes.

This exact scenario can occur when a caller tries to encrypt a final partial block, or when processing variable-length protocol messages that happen to be shorter than 16 bytes.

Reproduction Steps

#include <stdio.h>
#include <string.h>
#include "aes.h"

int main() {
    struct AES_ctx ctx;
    uint8_t key[16] = {0};
    uint8_t iv[16] = {0};

    // Only 5 bytes of actual data
    uint8_t small_buf[5] = {'H', 'e', 'l', 'l', 'o'};

    AES_init_ctx_iv(&ctx, key, iv);

    // This will read/write 16 bytes starting at small_buf,
    // overflowing 11 bytes past the end
    AES_CBC_encrypt_buffer(&ctx, small_buf, 5);

    return 0;
}

Compile with AddressSanitizer: gcc -fsanitize=address -o test_small test_small.c aes.c

ASan will report a stack-buffer-overflow on the XorWithIv or Cipher call.

Impact

When a caller passes a buffer smaller than 16 bytes with a non-zero length, up to 15 bytes of adjacent memory are corrupted (read, XORed, AES-encrypted, and written back). This can:

  • Crash the application (denial of service)
  • Corrupt adjacent stack variables, local data, or heap metadata
  • Potentially enable code execution if return addresses or function pointers are overwritten
  • Leak adjacent memory contents through the encrypted output (information disclosure)

Suggested Fix

Add a bounds check at the start of AES_CBC_encrypt_buffer():

void AES_CBC_encrypt_buffer(struct AES_ctx *ctx, uint8_t* buf, size_t length)
{
  if (length < AES_BLOCKLEN) return;  // Reject sub-block inputs
  // Or: length &= ~(AES_BLOCKLEN - 1);  // Truncate to block boundary
  // ... rest of function
}