Back to Blog
critical SEVERITY8 min read

How unbounded strcpy() causes heap buffer overflow in C NGINX modules and how to fix it

A critical buffer overflow vulnerability was discovered in the 51Degrees NGINX module (`ngx_http_51D_module.c`), where four uses of unbounded `strcpy()` allowed attackers to overflow fixed-size heap buffers by sending HTTP requests with oversized header names. The fix replaces all unsafe string operations with length-bounded NGINX-native alternatives (`ngx_memcpy` and `ngx_cpystrn`), preventing memory corruption without any change to functional behavior.

O
By Orbis AppSec
Published June 14, 2026Reviewed June 14, 2026

Answer Summary

This is a heap buffer overflow vulnerability (CWE-122) in the 51Degrees NGINX module, written in C. At four locations in `ngx_http_51D_module.c` (lines 1303, 1873, 2716, and 2801), the code used `strcpy()` to copy HTTP header names into fixed-size buffers with no length validation. An unauthenticated attacker could send an HTTP request with an oversized header name to overflow adjacent heap memory, potentially achieving remote code execution. The fix replaces all `strcpy()` and `strncat()` calls with NGINX's bounded equivalents — `ngx_memcpy()` and `ngx_cpystrn()` — which enforce strict byte limits on every copy operation.

Vulnerability at a Glance

cweCWE-122
fixReplaced strcpy()/strncat() with ngx_memcpy()/ngx_cpystrn() using pre-computed buffer lengths
riskUnauthenticated remote attacker can corrupt heap memory, potentially achieving code execution
languageC
root causestrcpy() used to copy user-controlled HTTP header names into fixed-size buffers with no length check
vulnerabilityHeap Buffer Overflow via unbounded strcpy()

How unbounded strcpy() causes heap buffer overflow in C NGINX modules and how to fix it

Introduction

The 51Degrees_module/ngx_http_51D_module.c file is the heart of the 51Degrees device detection NGINX module — it reads incoming HTTP request headers, extracts evidence for device fingerprinting, and populates response headers with detection results. But buried across four separate locations in this file, the same dangerous pattern repeated itself: HTTP header names provided by the outside world were being copied into fixed-size heap buffers using strcpy(), with zero validation of the input length.

Any unauthenticated attacker who can send an HTTP request to a server running this module could exploit this to corrupt heap memory — and potentially achieve remote code execution.


The Vulnerability Explained

What goes wrong with strcpy()

strcpy(dst, src) copies bytes from src to dst until it hits a null terminator. It does not know — and does not care — how large dst is. If src is longer than the allocated destination buffer, the copy continues past the end of that buffer, overwriting whatever memory comes next on the heap.

In ngx_http_51D_module.c, the vulnerable pattern appeared in the initRespHeaders() function at line 1303:

// VULNERABLE — before the fix
strcpy((char *)headerNames[respHeaderCount].data, headerName);

Here, headerNames[respHeaderCount].data is a buffer allocated based on an expected header name length. The variable headerName is derived from HTTP request data. If an attacker sends a request with a header name longer than what was allocated, strcpy() blows straight past the end of the buffer.

The same pattern appeared three more times:

Line 1389 — copying a property name into a response header struct:

// VULNERABLE
strcpy((char *)respHeader->property[respHeader->propertyCount]->data,
    propertyName);

Line 1870 — inside get_evidence(), copying a header name for query string evidence lookup:

// VULNERABLE
strcpy((char *)ngxHeaderName.data, headerName);

The add_value() function — using strncat() incorrectly:

// VULNERABLE — strncat length is not adjusted for existing content
strncat(dst, delimiter, length);
length -= strlen(delimiter);
strncat(dst, val, length);

The strncat() call passes length as the maximum number of characters to append, but length represents the total buffer size, not the remaining space after existing content. If dst already contains data, this calculation is wrong and a write past the buffer end is possible.

How an attacker exploits this

The attack surface is straightforward: HTTP header names. An attacker sends a crafted request like:

GET / HTTP/1.1
Host: target.example.com
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...[thousands of A's]: value

When NGINX routes this request through the 51Degrees module, initRespHeaders() processes the header name, allocates a buffer for it, and then calls strcpy() to copy the full (oversized) name into that undersized buffer. The excess bytes overwrite adjacent heap chunks — potentially corrupting allocator metadata, function pointers, or other live data structures.

On modern systems with heap hardening (ASLR, heap canaries), exploitation requires additional steps, but the memory corruption itself is reliable and deterministic. In worst-case scenarios on older or embedded deployments, this is a direct path to remote code execution from an unauthenticated HTTP request.


The Fix

The fix replaces every unsafe string operation with NGINX's own bounded memory functions, which require an explicit length argument and cannot write beyond the specified limit.

Fix 1 — initRespHeaders() at line 1303

// BEFORE
strcpy((char *)headerNames[respHeaderCount].data, headerName);

// AFTER
ngx_memcpy(headerNames[respHeaderCount].data, headerName, headerNames[respHeaderCount].len + 1);

ngx_memcpy is a thin wrapper around memcpy that copies exactly n bytes. The length used is headerNames[respHeaderCount].len + 1 — the pre-computed length of the header name (already stored in the struct) plus one for the null terminator. The copy cannot exceed what was allocated.

Fix 2 — initRespHeaders() at line 1389

// BEFORE
strcpy((char *)respHeader->property[respHeader->propertyCount]->data,
    propertyName);

// AFTER
ngx_memcpy(respHeader->property[respHeader->propertyCount]->data,
    propertyName, len + 1);

Same pattern: the already-computed len variable (used earlier to allocate the buffer) is now passed explicitly to bound the copy.

Fix 3 — get_evidence() at line 1870

// BEFORE
strcpy((char *)ngxHeaderName.data, headerName);

// AFTER
ngx_memcpy(ngxHeaderName.data, headerName, ngxHeaderName.len + 1);

Again, ngxHeaderName.len was already set before this line — the fix simply uses it to constrain the copy.

Fix 4 — add_value() function

// BEFORE
strncat(dst, delimiter, length);
length -= strlen(delimiter);
strncat(dst, val, length);

// AFTER
ngx_cpystrn((u_char *)(dst + ngx_strlen(dst)), (u_char *)delimiter, length + 1);
length -= ngx_strlen(delimiter);
ngx_cpystrn((u_char *)(dst + ngx_strlen(dst)), (u_char *)val, length + 1);

ngx_cpystrn(dst, src, n) copies at most n-1 characters and always null-terminates — it behaves like a correct strncpy. By computing the current end of dst with ngx_strlen(dst) and passing length + 1 as the limit, the function now correctly appends into only the remaining space in the buffer, regardless of how much content is already there.

Bonus fix — strtok_r safety

// BEFORE
char *tok, *tokPos = NULL;

// AFTER
char *tok, *tokPos = NULL, *saveptr = NULL;

A saveptr variable was added for thread-safe tokenization, a small but important correctness improvement alongside the primary fix.


Prevention & Best Practices

1. Never use strcpy() with externally-controlled input

In C, strcpy() should be treated as deprecated for any data that crosses a trust boundary. Replace it with:

Unsafe Safe alternative
strcpy(dst, src) ngx_memcpy(dst, src, known_len + 1) or strncpy with correct size
strcat(dst, src) ngx_cpystrn(dst + ngx_strlen(dst), src, remaining + 1)
strncat(dst, src, n) Compute remaining space explicitly before calling
sprintf(dst, ...) ngx_snprintf(dst, size, ...)

2. Use NGINX's string API throughout NGINX modules

NGINX provides a full set of safe string primitives: ngx_memcpy, ngx_cpystrn, ngx_snprintf, ngx_strncasecmp. These functions are designed for use in NGINX's memory model and always require explicit size parameters. Prefer them over raw C library functions in any NGINX module code.

3. Store computed lengths and use them

A key pattern in this fix is that the buffer lengths were already computed and stored in struct fields (headerNames[i].len, ngxHeaderName.len) — they just weren't being used to bound the copy. Make it a habit: if you allocate a buffer of size n, pass n to every function that writes into it.

4. Enable compiler and linker hardening

  • Compile with -D_FORTIFY_SOURCE=2 to enable glibc buffer overflow detection for common functions
  • Use -fstack-protector-strong for stack canaries
  • Link with -Wl,-z,relro,-z,now for read-only relocations
  • Enable AddressSanitizer (-fsanitize=address) in CI/test builds to catch overflows at runtime

5. Use static analysis in your CI pipeline

Tools that catch this class of vulnerability:

  • Semgrep: Rules for strcpy with tainted input — https://semgrep.dev/r?q=strcpy
  • CodeQL: cpp/unbounded-write query
  • Coverity: BUFFER_SIZE_WARNING, STRING_OVERFLOW detectors
  • Clang Static Analyzer: alpha.security.taint.TaintPropagation

Security standards

  • CWE-122: Heap-based Buffer Overflow
  • CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
  • OWASP: Buffer Overflow

Key Takeaways

  • strcpy() with HTTP header names is always dangerous: Header names in ngx_http_51D_module.c came directly from attacker-controlled HTTP requests. Any unbounded copy of this data is a critical vulnerability.
  • Pre-computed lengths must be used at copy time: The struct fields headerNames[i].len and ngxHeaderName.len existed and held correct values — the bug was that they were ignored during the strcpy() call.
  • strncat() is not a safe drop-in for strcat(): The add_value() function shows a classic strncat misuse where the length parameter doesn't account for existing buffer content. ngx_cpystrn with a pointer to the current end of the string is the correct pattern.
  • NGINX modules should use NGINX's string API: Mixing raw C library string functions with NGINX's memory pool model creates mismatches that are hard to audit. ngx_memcpy, ngx_cpystrn, and ngx_snprintf exist precisely for this.
  • Four instances of the same bug in one file signals a copy-paste pattern: When you fix one strcpy() in a codebase, search for all others in the same file — they likely share the same root cause and the same fix.

How Orbis AppSec Detected This

  • Source: HTTP request header names processed by the 51Degrees NGINX module — fully attacker-controlled, unauthenticated input
  • Sink: strcpy((char *)headerNames[respHeaderCount].data, headerName) at line 1303 in 51Degrees_module/ngx_http_51D_module.c, and three additional strcpy()/strncat() call sites at lines 1389, 1870, and in add_value()
  • Missing control: No length validation or bounds check before copying the user-supplied header name string into the fixed-size allocated buffer
  • CWE: CWE-122 — Heap-based Buffer Overflow
  • Fix: All four unsafe strcpy()/strncat() calls were replaced with ngx_memcpy() and ngx_cpystrn() using the pre-computed buffer lengths already available in the surrounding code

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

Four lines of strcpy() in a single C file were enough to expose every server running the 51Degrees NGINX module to unauthenticated heap corruption. The root cause wasn't complex — it was the classic C mistake of trusting that a source string will fit in its destination. The fix was equally straightforward: use the lengths that were already computed and pass them to bounded copy functions.

For developers writing or reviewing C code in NGINX modules, the lesson is concrete: treat strcpy() as a red flag in code reviews, use NGINX's own string API consistently, and run static analysis tools that specifically target unbounded writes with tainted input. Memory safety bugs at the HTTP parsing layer are among the most dangerous a web server can have — they're reachable before any authentication, from any network client, with a single malformed request.


References

Frequently Asked Questions

What is a heap buffer overflow?

A heap buffer overflow occurs when a program writes more data into a heap-allocated buffer than the buffer can hold, corrupting adjacent memory. In C, functions like strcpy() that do not check destination buffer size are a common cause.

How do you prevent buffer overflows in C NGINX modules?

Use NGINX's built-in bounded string functions such as ngx_memcpy(), ngx_cpystrn(), and ngx_snprintf() instead of raw C library functions like strcpy(), strcat(), or sprintf(). Always compute and enforce the destination buffer length before copying.

What CWE is a heap buffer overflow?

Heap buffer overflows are classified as CWE-122 (Heap-based Buffer Overflow), a subset of CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer).

Is strncat() enough to prevent buffer overflows?

Not always. strncat() is safer than strcat(), but its length parameter refers to the maximum number of characters to append — not the total destination buffer size. If the destination already contains data, it is easy to miscalculate the remaining space, as seen in the add_value() function in this vulnerability.

Can static analysis detect unbounded strcpy() vulnerabilities?

Yes. Static analysis tools such as Semgrep, CodeQL, and Coverity can detect unsafe uses of strcpy() and flag them as potential buffer overflow sources, especially when the input originates from external data like HTTP headers.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #372

Related Articles

critical

How buffer overflow in URL parsing happens in C++ HTTP client and how to fix it

A critical buffer overflow vulnerability in the HTTP client's URL parsing function allowed attackers to overflow a stack-allocated host buffer through specially crafted URLs with excessively long hostnames. The vulnerability enabled arbitrary code execution by overwriting the return address. The fix adds proper bounds validation before the memcpy() operation to ensure the hostname length never exceeds the destination buffer size.

critical

How heap buffer overflow happens in C WiFi frame capture and how to fix it

A critical buffer overflow vulnerability in the ESP32 WiFi frame capture feature (feat_capture_hs.c) allowed attackers within WiFi range to craft oversized 802.11 frames that would overflow heap buffers and achieve remote code execution. The fix adds explicit length validation before memcpy operations and rejects oversized frames rather than silently truncating them.

critical

How integer overflow in _wopendir() happens in C Windows dirent and how to fix it

A critical integer overflow vulnerability in `include/compat/dirent_msvc.h` allowed an attacker-controlled directory path length to wrap the `sizeof(wchar_t) * n + 16` allocation calculation, resulting in a dangerously undersized heap buffer. Subsequent writes to that buffer caused a heap overflow, enabling potential memory corruption or code execution on Windows systems. The fix adds a pre-allocation bounds check and proper errno signaling to safely reject overflow-inducing inputs.

critical

How buffer overflow in SCSI command handling happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in libretro-common's CDROM handling code where the `cdrom_send_command_win32()` function copied an arbitrary number of bytes into a fixed 16-byte SCSI Command Descriptor Block (CDB) buffer without validation. This vulnerability could allow an attacker using a malicious CDROM image or USB device to corrupt memory and potentially execute arbitrary code. The fix adds a simple bounds check before the memcpy operation to ensure cmd_len never exc

critical

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

A critical buffer overflow vulnerability in `kernel/filesystem.c` allowed malicious filesystem images to write beyond allocated buffer boundaries during header parsing. The fix adds proper bounds validation to ensure that sector data copies never exceed the allocated header buffer size, preventing heap corruption and potential code execution attacks.

critical

How buffer overflow happens in C xxd utility and how to fix it

A critical buffer overflow vulnerability was discovered in the xxd utility's `xxdline()` function where `strcpy()` was used without bounds checking on file input. An attacker could craft a malicious hex dump file with oversized lines to trigger memory corruption. The fix replaces the unsafe `strcpy()` with `snprintf()` to enforce buffer size limits.