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, 2026•Reviewed 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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #372

Related Articles

critical

How buffer overflow happens in C++ and how to fix it

A critical buffer overflow in `create_hex_string()` within `hmlangw.cpp` let an unconditional 16-iteration loop write past the bounds of a 100-byte `hex` buffer using unchecked `sprintf` calls. The fix replaces `sprintf` with `snprintf` and caps the loop iterations based on the actual destination buffer size, closing off a memory corruption path reachable from serial or network input.

critical

How Buffer Overflow via strcpy() Happens in C++ XML Parsers and How to Fix It

A critical buffer overflow vulnerability was discovered in `buildroot-external/package/libxmlparser/xmlParser.cpp`, where the `toXMLString` function used `_tcscpy()` to write XML escape sequences into a destination buffer without any bounds checking. An attacker supplying a crafted XML document could overflow the buffer and potentially execute arbitrary code. The fix replaces all five unsafe `_tcscpy()` calls with `memcpy()` calls that copy only the exact number of bytes required for each escape

critical

How Heap Buffer Overflows Happen in C++ ZIP Extraction and How to Fix Them

A critical heap buffer overflow vulnerability was discovered in `TKLiveSync/unzip.cpp`, where ZIP archive entry names were copied into a `PATH_MAX`-sized heap buffer using `strcpy()` without any length validation. Since the ZIP specification allows entry names up to 65,535 bytes — far exceeding typical `PATH_MAX` values of 1,024 to 4,096 bytes — a crafted archive could overflow the buffer and corrupt heap memory. The fix replaces the unsafe `strcpy`/`dirname` pattern with `std::string` operation

medium

How Integer Overflow happens in C++ image processing and how to fix it

A signed integer overflow in OpenCV's `bilateralFilter.cpp` allowed the buffer size calculation `cal_width * cal_height * cn` to wrap around to a small or negative value, causing `padding.resize()` to allocate far less memory than needed. Subsequent `memcpy` operations would then write beyond the allocated buffer, creating a heap corruption primitive. The fix is a single targeted cast to `size_t` that promotes the multiplication to unsigned 64-bit arithmetic before any overflow can occur.

critical

How Stack Buffer Overflows Happen in C with sprintf() and How to Fix Them

A critical stack buffer overflow was discovered in `libuv/Learn-libuv/docs/code/tty-gravity/main.c` where `sprintf()` wrote ANSI escape sequences and user-controlled variables into a fixed 500-byte buffer without any bounds checking. An attacker controlling the `pos`, `width`, or `message` variables could overflow the stack, overwrite return addresses, and potentially achieve arbitrary code execution. The fix replaces `sprintf()` with `snprintf()` and adds explicit length validation to ensure wr

high

How insecure string copy functions happen in C and how to fix them

A high-severity buffer overflow risk was discovered in `login/main.c` where `strcpy()` was used to copy the `HOME` environment variable into a fixed-size 512-byte buffer without any bounds checking. An attacker controlling the `HOME` environment variable could overflow `pwd_file_name`, potentially corrupting memory or hijacking execution. The fix replaces the two-step `strcpy`/`strcat` pattern with a single, bounds-safe `snprintf` call.