Back to Blog
high SEVERITY7 min read

Heap Buffer Overflow in stb_image.h: How a Missing Bounds Check Could Lead to Code Execution

A critical heap buffer overflow vulnerability was discovered and patched in a vendored copy of `stb_image.h`, a popular single-header image loading library. The root cause was a missing bounds check that allowed attacker-controlled image data to trigger memory writes beyond allocated heap buffers, potentially enabling arbitrary code execution. A single defensive guard — rejecting negative buffer lengths before any memory operation — closes this dangerous attack vector.

O
By Orbis AppSec
Published May 28, 2026Reviewed June 3, 2026

Answer Summary

A heap buffer overflow vulnerability in stb_image.h (CWE-122) allowed attackers to write beyond allocated heap memory through crafted image files. The root cause was missing validation of buffer length parameters before memory allocation and writes. The fix adds a bounds check that rejects negative or invalid buffer lengths before any memory operation occurs, preventing the overflow entirely.

Vulnerability at a Glance

cweCWE-122 (Heap-based Buffer Overflow)
fixAdd validation guard rejecting negative buffer lengths before memory operations
riskArbitrary code execution through memory corruption via malicious image files
languageC
root causeMissing bounds check on buffer length parameter before heap allocation
vulnerabilityHeap Buffer Overflow in Image Buffer Allocation

Heap Buffer Overflow in stb_image.h: How a Missing Bounds Check Could Lead to Code Execution

Introduction

Image parsing is one of the most dangerous attack surfaces in any application. Images are everywhere — uploaded by users, fetched from remote URLs, embedded in documents — and the code that decodes them is often decades old, written in C, and riddled with assumptions about input validity that simply don't hold in adversarial environments.

stb_image.h is a beloved single-header C library used by thousands of projects to load PNG, JPEG, BMP, GIF, and other image formats. Its simplicity is its appeal: drop one file into your project and you can parse images. But that same simplicity — a single vendored file that rarely gets updated — makes it a magnet for lingering vulnerabilities.

This post breaks down a critical heap buffer overflow found in a vendored copy of stb_image.h inside the hipster tool, explains how it could be exploited, and walks through the surgical one-line fix that closes the vulnerability.


The Vulnerability Explained

What Is a Heap Buffer Overflow?

A heap buffer overflow (CWE-120) occurs when a program writes data beyond the boundaries of a buffer allocated on the heap. Unlike stack overflows, heap overflows are subtler — they don't immediately crash the program in obvious ways — but they are equally dangerous. An attacker who controls what gets written, and where, can corrupt adjacent heap metadata, overwrite function pointers, or achieve arbitrary code execution.

Where Did This Vulnerability Live?

The vulnerable function is stbi__getn(), located at line 1405 of hipster/ext_src/stb/stb_image.h:

// BEFORE the fix
static int stbi__getn(stbi__context *s, stbi_uc *buffer, int n)
{
   if (s->io.read) {
      int blen = (int) (s->img_buffer_end - s->img_buffer);
      if (blen < n) {
         // ... reads from I/O and copies into buffer
      }
   }
   // ...
}

The parameter n represents the number of bytes to read into buffer. This value is derived directly from fields inside the image file being parsed — things like declared chunk sizes, compressed data lengths, or image dimensions.

Here's the problem: n is a signed integer, and there is no check that it is non-negative before use.

If an attacker crafts a malicious image file where a header field produces a negative value for n, several bad things can happen:

  1. The size comparison blen < n behaves incorrectly — a negative n will always be less than blen (a non-negative size), so the branch logic may be bypassed.
  2. Downstream memcpy or read operations that receive a negative n cast to size_t will interpret it as a massive unsigned integer (e.g., -1 becomes 0xFFFFFFFF on 32-bit or 0xFFFFFFFFFFFFFFFF on 64-bit), triggering a catastrophic out-of-bounds write.

The Attack Chain

Attacker crafts malicious image
        │
        ▼
Image header field encodes a negative or oversized length
        │
        ▼
stbi__getn() called with attacker-controlled n
        │
        ▼
No negative-value guard → size comparison bypassed
        │
        ▼
memcpy writes (size_t)(-1) bytes → heap overflow
        │
        ▼
Heap metadata corruption → potential arbitrary code execution

Real-World Impact

This class of vulnerability is well-documented in older stb_image releases and has been assigned multiple CVEs over the years. The impact depends on context:

  • Remote Code Execution (RCE): If the application processes images from untrusted sources (uploads, URLs), an attacker can deliver a crafted image and potentially execute arbitrary code in the process.
  • Denial of Service (DoS): At minimum, the heap corruption will crash the application.
  • Privilege Escalation: If the vulnerable process runs with elevated privileges, code execution becomes especially dangerous.

The hipster tool bundles this library as a vendored copy — meaning it doesn't benefit from upstream patches unless someone manually updates the file. This is a common and dangerous pattern with single-header libraries.


The Fix

What Changed

The fix is elegantly minimal — a single guard clause at the top of stbi__getn():

// AFTER the fix
static int stbi__getn(stbi__context *s, stbi_uc *buffer, int n)
{
+  if (n < 0) return 0;   // ← THE FIX
   if (s->io.read) {
      int blen = (int) (s->img_buffer_end - s->img_buffer);
      if (blen < n) {
         // ...
      }
   }
   // ...
}

Before: Any caller could pass a negative n, which would silently bypass size checks and potentially trigger a massive out-of-bounds memory operation.

After: If n is negative, the function immediately returns 0 (indicating zero bytes read) without touching the buffer. The caller receives a safe failure signal and can handle it gracefully.

Why This Works

The fix enforces a precondition: byte counts must be non-negative. This is a classic example of input validation at the trust boundary — the point where external, attacker-controlled data (the image file) influences internal program behavior.

By rejecting the invalid input before any memory operation occurs, the vulnerability is neutralized regardless of what the image file contains. The fix is:

  • Minimal — one line, no logic changes elsewhere
  • Correct — a request to read a negative number of bytes is always invalid
  • Safe — returns a failure code rather than crashing or corrupting memory
  • Non-breaking — no legitimate caller should ever pass n < 0

The Diff at a Glance

 static int stbi__getn(stbi__context *s, stbi_uc *buffer, int n)
 {
+   if (n < 0) return 0;
    if (s->io.read) {
       int blen = (int) (s->img_buffer_end - s->img_buffer);
       if (blen < n) {

One line. One character of indentation. One critical security boundary enforced.


Prevention & Best Practices

1. Always Validate Lengths Before Memory Operations

Any time a length or size value originates from external input (files, network, user input), validate it before use:

// Bad: trust the file header
int chunk_size = read_int_from_file(f);
memcpy(dest, src, chunk_size);  // ← dangerous!

// Good: validate first
int chunk_size = read_int_from_file(f);
if (chunk_size < 0 || chunk_size > MAX_ALLOWED_SIZE) {
    return ERROR_INVALID_INPUT;
}
memcpy(dest, src, chunk_size);  // ← safe

2. Prefer size_t for Sizes — But Validate Signed Inputs First

memcpy, malloc, and similar functions take size_t (unsigned). When you cast a signed integer to size_t, negative values silently become enormous numbers. Always validate before casting:

// Dangerous implicit cast
void read_data(int n) {
    char *buf = malloc(n);          // if n < 0, malloc((size_t)-1) → allocation failure or huge alloc
    memcpy(buf, src, n);            // if n < 0, writes gigabytes
}

// Safe pattern
void read_data(int n) {
    if (n <= 0 || n > MAX_SAFE_SIZE) return;
    char *buf = malloc((size_t)n);  // now safe
    if (!buf) return;
    memcpy(buf, src, (size_t)n);    // now safe
}

3. Keep Vendored Libraries Updated

Single-header libraries like stb_image.h are easy to vendor but easy to forget. Establish a process:

  • Track the upstream version you've vendored (add a comment at the top of the file)
  • Subscribe to security advisories for the library (GitHub security advisories, OSV.dev)
  • Automate dependency scanning — tools like osv-scanner, trivy, or grype can detect known-vulnerable vendored files
  • Schedule periodic reviews of vendored code, especially image/audio/video parsers

4. Use Memory-Safe Wrappers Where Possible

If your language or framework provides safer alternatives, prefer them:

  • Use AddressSanitizer (ASan) during development and testing to catch buffer overflows at runtime
  • Use fuzzing (libFuzzer, AFL++) on image parsing code — this exact class of bug is highly fuzz-detectable
  • Consider sandboxing image parsing in a separate process with limited privileges

5. Reference Security Standards

This vulnerability maps to well-known security weaknesses:

Standard Reference
CWE CWE-120: Buffer Copy without Checking Size of Input
CWE CWE-787: Out-of-bounds Write
OWASP A03:2021 – Injection
OWASP Input Validation Cheat Sheet

6. Tools to Detect This Class of Issue

  • Static Analysis: cppcheck, clang-tidy, Coverity, CodeQL
  • Dynamic Analysis: AddressSanitizer (-fsanitize=address), Valgrind
  • Fuzzing: libFuzzer, AFL++, Honggfuzz
  • Dependency Scanning: osv-scanner, trivy, grype

Conclusion

A single missing bounds check — if (n < 0) return 0; — was all that stood between a functioning image loader and a critical heap buffer overflow. This is a humbling reminder that:

  1. The most dangerous vulnerabilities are often the simplest. One missing guard, one implicit cast, one forgotten validation.
  2. Vendored code is your code. When you copy a library into your repository, you own its bugs. Keep it updated.
  3. Image parsers are high-value targets. They process untrusted binary data, they're written in C, and they're often ancient. Treat them with extra scrutiny.
  4. Defense in depth matters. Input validation, memory-safe tooling, fuzzing, and sandboxing each add a layer of protection. No single measure is sufficient.

The fix here was automated, verified, and deployed — but the underlying lesson is one every C/C++ developer should internalize: never trust a length that came from outside your process. Validate it. Clamp it. Reject it if it's wrong. Your heap will thank you.


This vulnerability was identified and fixed by OrbisAI Security. Automated security scanning, triage, and patching — so your team can focus on building.

Frequently Asked Questions

What is a heap buffer overflow?

A heap buffer overflow occurs when a program writes data beyond the boundaries of an allocated heap buffer, corrupting adjacent memory and potentially enabling arbitrary code execution.

How do you prevent heap buffer overflow in C?

Always validate buffer sizes and bounds before allocation and write operations, use safe APIs like snprintf() instead of sprintf(), enable compiler protections (stack canaries, ASLR), and use memory sanitizers during testing.

What CWE is heap buffer overflow?

CWE-122 (Heap-based Buffer Overflow) and its parent category CWE-120 (Buffer Copy without Checking Size of Input).

Is bounds checking in one location enough to prevent heap buffer overflow?

No, comprehensive bounds validation is needed at all entry points where untrusted data influences buffer sizes, but strategic checks before critical operations like allocation can prevent the most dangerous variants.

Can static analysis detect heap buffer overflow?

Yes, modern static analysis tools can detect many heap buffer overflow patterns by tracking buffer allocations and writes, though some cases require runtime analysis or fuzzing to discover.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #13

Related Articles

critical

How buffer overflow happens in C tar header parsing and how to fix it

A critical buffer overflow vulnerability was discovered in `microtar/microtar.c` where the `raw_to_header()` and `header_to_raw()` functions used unbounded `strcpy()` and `sprintf()` calls to copy tar header fields. Malicious tar files with non-null-terminated name fields could overflow destination buffers, potentially leading to code execution. The fix replaces all unsafe string operations with bounded alternatives: `memcpy()` with explicit null-termination and `snprintf()` instead of `sprintf(

critical

How buffer overflow happens in C ieee80211_input() and how to fix it

A critical buffer overflow vulnerability was discovered in `src/firmware/src/net/ieee80211.c` at line 1584, where the `ieee80211_input()` function processed raw 802.11 data frames without verifying that the incoming frame was large enough to contain a valid `ieee80211_frame` header. An attacker within wireless range could craft undersized or malformed frames to trigger memory corruption, potentially leading to remote code execution on the firmware. The fix adds a single, targeted bounds check th

high

How buffer overflow from unsafe string copy functions happens in C network interface code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `generic/eth-impl.c`, where unsafe `strncpy()` and `sprintf()` calls could write beyond buffer boundaries when handling network interface names and device filenames. The fix replaced these dangerous functions with bounded `snprintf()` calls that guarantee null-termination and prevent memory corruption.

critical

How buffer overflow in FuzzIxml.c sprintf() happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in `fuzzer/FuzzIxml.c` where `sprintf()` wrote a PID-formatted filename into a fixed 256-byte stack buffer without any bounds checking. The fix replaces `sprintf()` with `snprintf()`, explicitly passing the buffer size to prevent any overflow. While exploitation in this specific fuzzer context requires local access, the pattern is a textbook example of CWE-120 that developers should recognize and eliminate everywhere it appears.

critical

How buffer overflow happens in C HTML parsing and how to fix it

A critical buffer overflow vulnerability in `include/html_parse.h` allowed attackers to overflow buffers by providing malicious HTML input exceeding buffer capacity. The fix adds proper bounds checking before memcpy() operations to prevent memory corruption and potential code execution.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.