Back to Blog
critical SEVERITY6 min read

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

A critical buffer overflow vulnerability was discovered in the `daemonize()` function of `tpl.c`, where command-line arguments are concatenated into a fixed-size 8192-byte buffer using `strcat()` without any bounds checking. An attacker who controls command-line arguments can overflow this buffer to corrupt adjacent memory and potentially achieve arbitrary code execution. The fix adds a buffer-length check before each concatenation to ensure writes never exceed the declared buffer size.

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

Answer Summary

This is a classic CWE-120 buffer overflow in C caused by using `strcat()` without bounds checking in the `daemonize()` function of `tpl.c`. Command-line arguments are concatenated into a fixed 8192-byte buffer, and if total argument length exceeds this limit, memory corruption occurs. The fix adds a length check before each `strcat()` call to ensure the cumulative string never exceeds the buffer's declared size, preventing overflow regardless of input length.

Vulnerability at a Glance

cweCWE-120
fixAdd cumulative length validation before each strcat() call
riskArbitrary code execution via memory corruption
languageC
root causeNo bounds checking before strcat() into fixed-size buffer in daemonize()
vulnerabilityBuffer overflow via unbounded strcat()

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

Introduction

The tpl.c file implements a template processing CLI tool with a daemonize() function that reconstructs command-line arguments into a single string for re-execution as a background process. At line 70, this function uses a fixed-size char buf[8192] and iterates over argv[], calling strcat(buf, argv[i]) for each argument—without ever checking whether the accumulated string still fits within the 8192-byte boundary.

This is a textbook CWE-120 buffer overflow: a local attacker who controls command-line arguments (or an upstream process that passes crafted arguments) can supply inputs totaling more than 8KB, overwriting stack memory beyond buf and potentially hijacking control flow.

The vulnerability was flagged as critical because the overflow is trivially exploitable and the tool runs in production environments where argument sources may not be fully trusted.


The Vulnerability Explained

The vulnerable pattern in daemonize() looks like this:

void daemonize(int argc, char *argv[]) {
    char buf[8192];
    buf[0] = '\0';

    for (int i = 0; i < argc; i++) {
        strcat(buf, argv[i]);
        strcat(buf, " ");
    }
    // ... fork and exec with buf ...
}

Why this is dangerous:

  1. buf is allocated on the stack with a fixed size of 8192 bytes.
  2. strcat() appends data to the end of the existing string and writes a null terminator—it has no concept of the destination buffer's capacity.
  3. Each iteration blindly appends argv[i] plus a space character without checking how much room remains.
  4. If the sum of all argument lengths (plus spaces and null terminator) exceeds 8192, strcat() writes past the end of buf.

Concrete exploitation scenario:

An attacker runs:

tpl -d $(python -c "print('A'*9000)")

This passes a single 9000-byte argument. Combined with "tpl", "-d", and the space separators, the total exceeds 8192 bytes. The overflow corrupts the saved return address on the stack. A sophisticated attacker can craft the overflow payload to redirect execution to shellcode or a ROP chain, achieving arbitrary code execution with the privileges of the tpl process.

Threat model context: While tpl is a local CLI tool (meaning the attacker needs local access or control over how the tool is invoked), many deployment scenarios involve wrapper scripts, cron jobs, or orchestration systems that pass arguments from external sources—making this a realistic attack surface.


The Fix

The fix adds a cumulative length check before each strcat() call, ensuring the buffer is never written beyond its declared size. Here's the before/after comparison:

Before (vulnerable):

void daemonize(int argc, char *argv[]) {
    char buf[8192];
    buf[0] = '\0';

    for (int i = 0; i < argc; i++) {
        strcat(buf, argv[i]);
        strcat(buf, " ");
    }
    // ...
}

After (fixed):

void daemonize(int argc, char *argv[]) {
    char buf[8192];
    size_t remaining = sizeof(buf);
    buf[0] = '\0';

    for (int i = 0; i < argc; i++) {
        size_t arg_len = strlen(argv[i]) + 1; /* +1 for space */
        if (arg_len >= remaining) {
            break; /* or handle error: truncate rather than overflow */
        }
        strcat(buf, argv[i]);
        strcat(buf, " ");
        remaining -= arg_len;
    }
    // ...
}

Key aspects of the fix:

  1. Tracks remaining capacity: A remaining variable is initialized to sizeof(buf) and decremented after each successful append.
  2. Pre-checks before write: Before calling strcat(), the code verifies that argv[i] plus the space separator fits within the remaining capacity.
  3. Fails safely: If arguments would overflow the buffer, the loop breaks—truncating the command rather than corrupting memory.
  4. Enforces the security invariant: "Buffer reads never exceed the declared length."

The accompanying regression test (tests/test_invariant_tpl.c) exercises three scenarios:
- A 9000-byte argument (exceeds buffer) — must not crash
- An 8192-byte argument (boundary value) — must not overflow
- A small normal argument — must work correctly

This ensures the fix handles edge cases and prevents future regressions.


Prevention & Best Practices

1. Never use unbounded string functions with external input:
- Replace strcat() with strncat() or snprintf() which accept a maximum length parameter.
- Better yet, use snprintf() which returns the number of characters that would have been written, making truncation detection trivial.

2. Prefer dynamic allocation for variable-length data:

// Safer approach: calculate needed size first
size_t total = 0;
for (int i = 0; i < argc; i++)
    total += strlen(argv[i]) + 1;
char *buf = malloc(total + 1);

3. Enable compiler protections:
- Compile with -fstack-protector-strong to detect stack buffer overflows at runtime.
- Use -D_FORTIFY_SOURCE=2 which replaces strcat with a bounds-checked version when the buffer size is known at compile time.

4. Static analysis:
- Run tools like cppcheck, Coverity, or Semgrep with rules targeting strcat() into fixed-size buffers.
- Enable -Wall -Wextra to catch related warnings.

5. Use AddressSanitizer during testing:

gcc -fsanitize=address -g tpl.c -o tpl_test

This catches overflows immediately during test execution.


Key Takeaways

  • Never use strcat() in a loop without tracking cumulative buffer usage — the daemonize() function's pattern of iterating over argv[] and appending to a fixed buffer is a classic overflow recipe.
  • An 8192-byte buffer is not "large enough" — attackers craft inputs specifically to exceed whatever size you chose; only explicit bounds checking is safe.
  • CLI tools are not immune to exploitation — even though tpl requires local access, automated pipelines and orchestration systems can pass attacker-controlled arguments.
  • Regression tests for buffer boundaries catch future mistakes — the test at tests/test_invariant_tpl.c exercises the exact overflow scenario and boundary condition.
  • The security invariant "buffer reads never exceed the declared length" should be enforced programmatically, not assumed by convention.

How Orbis AppSec Detected This

  • Source: Command-line arguments (argv[]) passed to the tpl binary
  • Sink: strcat(buf, argv[i]) in daemonize() at tpl.c:70, writing into a stack-allocated char buf[8192]
  • Missing control: No bounds checking or remaining-capacity tracking before each strcat() call
  • CWE: CWE-120 — Buffer Copy without Checking Size of Input
  • Fix: Added a cumulative length check that validates strlen(argv[i]) + 1 < remaining before each concatenation, breaking the loop if the buffer would overflow

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

This buffer overflow in tpl.c's daemonize() function is a stark reminder that C's string functions provide zero safety guarantees—the programmer must enforce bounds manually. The fix is conceptually simple (check length before writing), but the consequences of missing it are severe: memory corruption, crashes, and potential arbitrary code execution.

If you're writing C code that handles variable-length input—whether from command-line arguments, files, or network data—always track your buffer's remaining capacity and validate before every write. Use bounded alternatives like snprintf(), enable compiler hardening flags, and write regression tests that specifically exercise boundary conditions.


References

Frequently Asked Questions

What is a buffer overflow via strcat()?

A buffer overflow via strcat() occurs when data is appended to a fixed-size character array without verifying that the combined string length fits within the buffer's allocated memory, causing writes beyond the buffer boundary that corrupt adjacent memory.

How do you prevent buffer overflow in C?

Use bounded string functions like strncat() or snprintf(), always track remaining buffer capacity before writes, and validate total input length against buffer size before concatenation operations.

What CWE is buffer overflow?

CWE-120 (Buffer Copy without Checking Size of Input) covers cases where data is copied into a buffer without verifying that the source data fits, which is exactly what happens with unchecked strcat() calls.

Is using a large buffer enough to prevent buffer overflow?

No. Even an 8192-byte buffer can be overflowed if input is unconstrained. The only reliable prevention is explicit bounds checking regardless of buffer size, since attackers can always craft inputs larger than any fixed allocation.

Can static analysis detect buffer overflow from strcat()?

Yes. Static analysis tools and linters can flag strcat() usage into fixed-size buffers as potentially unsafe, especially when the source data comes from user-controlled inputs like command-line arguments.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1053

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

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.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.