Integer Overflow to Heap Buffer Overflow: How a Missing Size Check Almost Took Down an Embedded Web Server
Introduction
There's a classic category of bugs in C that security engineers have been fighting for decades: the integer overflow that silently corrupts memory. These bugs are deceptively simple — a single missing bounds check, a type mismatch, or an arithmetic operation that wraps around — and yet they can be catastrophic. Today we're examining exactly this kind of vulnerability, discovered and patched in an embedded web server written in C for an ESP-based IoT device.
This post walks through a critical-severity heap buffer overflow (CWE-120/CWE-122) triggered by an integer overflow (CWE-190) in the HTTP request handling code. If you write C or C++ — especially for embedded or resource-constrained systems — this is a pattern you need to recognize and avoid.
The Vulnerability Explained
What Happened?
The web server reads HTTP POST request bodies by trusting the Content-Length header. The problematic code looked like this:
// VULNERABLE CODE (before fix)
int len = r->content_len;
if (len <= 0 || len > 4096)
return httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, "Bad length"), ESP_FAIL;
char *buf = malloc(len + 1); // ← Danger zone
At first glance, this looks reasonable. There's even a bounds check! But the devil is in the details.
The Integer Overflow Path
Here's the problem: r->content_len is a signed integer (int), but malloc() takes a size_t, which is an unsigned type. On a 32-bit system, SIZE_MAX is 0xFFFFFFFF (4,294,967,295).
Consider what happens when content_len is set to the maximum value of a signed 32-bit integer (INT_MAX = 0x7FFFFFFF = 2,147,483,647):
- The bounds check
len > 4096catches this — ✅ good so far.
But what if the underlying HTTP parser stores content_len as a value that, when cast to size_t, becomes enormous? Or what if a future refactor changes the type? The deeper architectural issue is that arithmetic on int before passing to malloc() is inherently unsafe in this context.
The classic exploit path is:
Content-Length: 4294967295 (0xFFFFFFFF as size_t)
If len is treated as size_t and equals SIZE_MAX (0xFFFFFFFF), then:
malloc(len + 1)
// = malloc(0xFFFFFFFF + 1)
// = malloc(0x100000000)
// On a 32-bit system: malloc(0) ← wraps to zero!
malloc(0) is implementation-defined — it may return a non-NULL pointer to a zero-byte allocation, or it may return NULL. Either way, the subsequent read loop:
int rx = 0;
while (rx < len) {
int n = httpd_req_recv(r, buf + rx, len - rx);
rx += n;
}
...attempts to read up to len bytes (potentially gigabytes worth of intent) into a buffer that holds zero usable bytes. This is a textbook heap buffer overflow.
Real-World Impact
On an embedded ESP device, heap corruption can lead to:
- Remote Code Execution (RCE): By carefully crafting the overflow, an attacker may control adjacent heap metadata or function pointers.
- Denial of Service (DoS): Corrupting the heap allocator's internal structures causes a crash on the next allocation or free.
- Sensitive Data Exposure: Overwriting adjacent heap objects may expose credentials, session tokens, or configuration data.
- Device Takeover: On IoT devices with no memory protection (no MMU, no ASLR), heap overflows are often directly exploitable.
Attack Scenario
An attacker on the same network (or with access to the device's HTTP port) sends a crafted POST request:
POST /api/settings HTTP/1.1
Host: 192.168.1.100
Content-Length: 4294967295
Authorization: Bearer <valid-token>
<attacker-controlled payload>
The server allocates a near-zero buffer, then copies the attacker's payload into it, corrupting the heap. From here, a skilled attacker can pivot to arbitrary code execution on the microcontroller.
The Fix
What Changed?
The fix is elegant and follows secure C coding best practices precisely. Here's the corrected code:
// FIXED CODE (after patch)
// Step 1: Validate using the SIGNED type first (catches negative/absent header)
if (r->content_len <= 0 || r->content_len > 4096)
return httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, "Bad length"), ESP_FAIL;
// Step 2: Widen to size_t AFTER validation — now safe
size_t len = (size_t)r->content_len;
char *buf = malloc((size_t)len + 1);
if (!buf)
return httpd_resp_send_err(r, HTTPD_500_INTERNAL_SERVER_ERROR, "OOM"), ESP_FAIL;
size_t rx = 0;
while (rx < len) {
int n = httpd_req_recv(r, buf + rx, len - rx);
if (n <= 0) { free(buf); return ESP_FAIL; }
rx += (size_t)n;
}
buf[len] = '\0';
The same pattern was applied to the read_json_body() helper function:
// FIXED read_json_body()
if (r->content_len <= 0 || (size_t)r->content_len > max_len) {
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, "Invalid body length");
return NULL;
}
size_t len = (size_t)r->content_len;
char *buf = malloc((size_t)len + 1);
Why This Fix Works
The fix follows the "validate first, widen later" principle — a cornerstone of safe C programming:
| Step | Action | Why It's Safe |
|---|---|---|
| 1. Validate signed | Check content_len <= 0 and content_len > 4096 using signed comparison |
Catches negative values and out-of-range values before any casting |
2. Widen to size_t |
Cast to size_t only after validation passes |
At this point, value is guaranteed to be in [1, 4096], so size_t cast is safe |
3. Use size_t throughout |
rx, loop comparisons, and arithmetic all use size_t |
Eliminates mixed signed/unsigned arithmetic that causes subtle overflow bugs |
The key insight: size_t arithmetic cannot wrap to zero when the value has already been validated to be ≤ 4096. malloc(4096 + 1) will always allocate 4097 bytes — exactly what we need.
Prevention & Best Practices
1. Validate Before You Widen
Always validate untrusted integer values using their native type before casting to a wider or unsigned type:
// ❌ BAD: Cast first, validate later (or never)
size_t len = (size_t)untrusted_input;
if (len > MAX_LEN) { ... } // Too late — overflow already possible
// ✅ GOOD: Validate first, widen after
if (untrusted_input <= 0 || untrusted_input > MAX_LEN) { ... }
size_t len = (size_t)untrusted_input; // Now safe
2. Never Trust HTTP Headers Directly
HTTP headers are attacker-controlled. Treat Content-Length, Content-Type, and any other header as untrusted input requiring validation before use in memory operations.
3. Use size_t for Sizes and Lengths
Whenever you're working with memory sizes, buffer lengths, or loop counters tied to memory operations, use size_t. Mixing int and size_t in arithmetic is a common source of overflow bugs.
// ❌ Risky: int arithmetic fed to malloc
int len = get_length();
char *buf = malloc(len + 1);
// ✅ Safe: size_t arithmetic after validation
if (len <= 0 || len > MAX) return error;
size_t safe_len = (size_t)len;
char *buf = malloc(safe_len + 1);
4. Enable Compiler Warnings
Modern compilers can catch many of these issues. Enable and heed these flags:
# GCC / Clang
-Wall -Wextra -Wconversion -Wsign-conversion -Wshadow
# For embedded (ESP-IDF)
# Add to CMakeLists.txt:
target_compile_options(${COMPONENT_LIB} PRIVATE -Wconversion -Wsign-conversion)
5. Use Static Analysis Tools
Several tools can automatically detect integer overflow → buffer overflow chains:
- Coverity — Industry-standard static analyzer, free for open source
- CodeQL — GitHub's semantic code analysis engine (detects CWE-190, CWE-122)
- Semgrep — Fast, customizable pattern-based scanner
- clang-tidy — Includes
bugprone-integer-division,cert-int30-c - OrbisAI Security — AI-powered scanner that detected this exact vulnerability
6. Consider AddressSanitizer During Development
For embedded targets that support it, or during host-based unit testing:
# Compile with AddressSanitizer
gcc -fsanitize=address,undefined -g -o server_test server_test.c
# Run your tests — heap overflows will be caught immediately
./server_test
7. Reference Security Standards
This vulnerability maps to well-documented weaknesses:
- CWE-190: Integer Overflow or Wraparound
- CWE-122: Heap-based Buffer Overflow
- CWE-120: Buffer Copy without Checking Size of Input
- CERT C Rule INT30-C: Ensure unsigned integer operations do not wrap
- OWASP: Buffer Overflow: General guidance on buffer overflow prevention
Conclusion
This vulnerability is a perfect illustration of why C programming for embedded systems demands extreme care with integer types. The original code wasn't obviously wrong — it had a bounds check! — but the subtle interaction between signed integers, unsigned memory sizes, and 32-bit arithmetic created a critical exploitable path.
The fix is just a few lines of code, but it embodies a principle that every C developer should internalize: validate in the type you received, widen only after you're sure it's safe.
Key takeaways:
- 🔴 HTTP headers are attacker-controlled — never use them directly in memory operations
- 🔴 Integer overflow is silent —
malloc(len + 1)won't warn you whenlen + 1wraps to 0 - 🟢 Validate signed, then widen to
size_t— this is the safe pattern for C memory allocation - 🟢 Use compiler warnings and static analysis — tools like CodeQL and OrbisAI can catch these automatically
- 🟢 Test with AddressSanitizer — it will catch heap overflows that slip past code review
Heap buffer overflows remain one of the most dangerous and exploitable vulnerability classes in C code. In embedded systems — where there's often no OS-level memory protection, no ASLR, and no sandboxing — a single heap overflow can mean complete device compromise.
Write the bounds check. Validate before you widen. Your future self (and your users) will thank you.
This vulnerability was automatically detected and patched by OrbisAI Security. Automated security scanning catches issues like this before they reach production.