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

medium

How integer overflow in bounds checking happens in C and how to fix it

A critical integer overflow vulnerability was discovered in the W_Read function of DOOM/w_file.c that allowed attackers to bypass bounds checking by crafting WAD files with malicious offset values near UINT_MAX. The fix implements a two-step validation approach that first checks if the offset exceeds the file length, then safely calculates the remaining bytes without risk of overflow.

critical

How command injection happens in Node.js subprocess and how to fix it

A critical command injection vulnerability in `tools/dev/src/index.ts` allowed attackers to execute arbitrary shell commands through unsanitized subprocess arguments. The fix was simple but essential: explicitly setting `shell: false` in the `spawn()` call to prevent shell metacharacter interpretation. This vulnerability demonstrates why subprocess handling requires explicit security controls in Node.js.

critical

How GitHub token exposure happens in TypeScript CLI utilities and how to fix it

A critical credential exposure vulnerability was discovered in `cli/src/utils/github.ts`, where three GitHub API fetch calls were made without any safe token-loading mechanism, risking accidental hardcoding or token leakage in logs and CI/CD pipelines. The fix introduces a centralized `getAuthHeaders()` function that reads the token exclusively from the `GITHUB_TOKEN` environment variable and safely injects it into all outbound API requests. This ensures credentials never touch source code, buil

high

How improper handling of case sensitivity happens in Go MCP SDK and how to fix it

A high-severity vulnerability (CVE-2026-27896) in the Model Context Protocol Go SDK v1.3.0 allowed attackers to bypass security controls through improper handling of case sensitivity. The fix upgrades the dependency from v1.3.0 to v1.3.1, which correctly normalizes case comparisons. This vulnerability was particularly concerning for CLI tools where attackers could manipulate input to evade validation logic.

high

How buffer overflow via insecure strcpy/strncpy happens in C textbox widgets and how to fix it

A high-severity buffer overflow vulnerability was discovered in the Aroma UI framework's textbox widget where `strncpy()` was used to copy user-provided text without guaranteed null-termination safety. The fix replaces the dangerous `strncpy()` pattern with `snprintf()`, which automatically handles buffer boundaries and null-termination in a single, safer operation.

critical

How command injection happens in Python os.system() and how to fix it

A critical command injection vulnerability was discovered in `src/O4_Geotag.py` where file paths and coordinate values were concatenated directly into `os.system()` calls invoking `gdal_translate` and `gdalwarp`. Because `os.system()` passes its argument through a shell interpreter, any shell metacharacters in the file path variable `f` — sourced from file enumeration or user-supplied input — could be exploited to execute arbitrary commands. The fix replaces both shell invocations with direct ca