strcpy, strcat and sprintf: bounded replacements that are actually safe

`strcpy`, `strcat` and `sprintf` take no destination size, so they write until they find a NUL byte in the source — an attacker-controlled length becomes a stack or heap overwrite. Replace them with `snprintf` for formatting, and `strlcpy`/`strlcat` (or `snprintf(dst, size, "%s", src)` where those are unavailable) for copies. The bounded functions are only safe if you check the result: `snprintf` returns the length it *would* have written, `strlcpy` returns the source length, and both silently truncate when that exceeds the buffer. `strncpy` is not a safe `strcpy` — it does not NUL-terminate on truncation.

At a glance

LanguagesC, C++ (and any FFI boundary that hands a raw buffer to C)
Unbounded functionsstrcpy, strcat, sprintf, vsprintf, gets, scanf("%s")
Bounded replacementssnprintf, vsnprintf, strlcpy, strlcat, memcpy_s, fgets
Deceptively unsafestrncpy (no NUL on truncation), strncat (bound is remaining space, not total), sizeof on a pointer parameter
Typical impactReturn-address or vtable overwrite leading to code execution; adjacent-field corruption; crash
Compiler help-D_FORTIFY_SOURCE=3 -fstack-protector-strong -Wformat-security -fsanitize=address

Vulnerable and fixed, side by side

C — copying a string

Vulnerable

void handle(const char *user_input) {
    char name[64];
    strcpy(name, user_input);   /* writes strlen(user_input)+1 bytes */
    log_name(name);
}

Secure

int handle(const char *user_input) {
    char name[64];
    int n = snprintf(name, sizeof name, "%s", user_input);
    if (n < 0 || (size_t)n >= sizeof name) {
        return -1;              /* truncated — reject rather than proceed */
    }
    log_name(name);
    return 0;
}

snprintf always NUL-terminates and returns the length it would have written, so `n >= sizeof name` is the truncation test. Ignoring the return value converts an overflow into silent data loss, which is better but still a bug.

C — the strncpy trap

Vulnerable

char dst[16];
strncpy(dst, src, sizeof dst);   /* no NUL if strlen(src) >= 16 */
printf("%s\n", dst);            /* reads past the buffer */

Secure

char dst[16];
if (strlcpy(dst, src, sizeof dst) >= sizeof dst) {
    return -1;                   /* source did not fit */
}

/* Where strlcpy is unavailable (glibc before 2.38, MSVC): */
char dst2[16];
dst2[0] = '\0';
if (snprintf(dst2, sizeof dst2, "%s", src) >= (int)sizeof dst2) {
    return -1;
}

strncpy was designed for fixed-width records, not for strings: it pads with NUL when the source is short and omits the terminator when the source is long. Neither behaviour is what a caller replacing strcpy expects.

C — sizeof across a function boundary

Vulnerable

void copy_into(char *dst, const char *src) {
    /* dst is a pointer here; sizeof dst is 8, not the array's size. */
    snprintf(dst, sizeof dst, "%s", src);
}

Secure

void copy_into(char *dst, size_t dst_size, const char *src) {
    snprintf(dst, dst_size, "%s", src);
}

/* Call site keeps the size next to the array, where sizeof still works. */
char buf[128];
copy_into(buf, sizeof buf, src);

This is the most common way a correctly-written bounded call still overflows. Pass the size alongside every buffer; `-Wsizeof-pointer-memaccess` catches some cases but not all.

How to find it in your codebase

  • Grep the unbounded set: `rg -n '\b(strcpy|strcat|sprintf|vsprintf|gets)\s*\('`. There is no safe use of `gets` at all — it was removed in C11.
  • Build with `-D_FORTIFY_SOURCE=3 -O2 -Wall -Wextra -Wformat-security -Wstringop-overflow`; fortified glibc aborts on many overflows at runtime rather than corrupting memory.
  • Run the test suite under `-fsanitize=address,undefined`. ASan finds the overflows that only trigger on long inputs, which is precisely the attacker's case.
  • Semgrep `c.lang.security.insecure-use-strcat-fn` and `c.lang.security.insecure-use-printf-fn`; clang-tidy `bugprone-not-null-terminated-result` and `cert-err33-c` for ignored return values.
  • Where the code is new rather than legacy, `std::string` / `std::format` in C++ and `Vec<u8>`/`String` in Rust remove the class rather than bounding it.

Fix checklist

  1. Replace each unbounded call with its bounded counterpart, passing an explicit destination size.
  2. Check the return value of every bounded call and decide, per call site, whether truncation is an error or acceptable. Silent truncation in a path check or an authorisation string is itself a vulnerability.
  3. Make sure the size expression is not `sizeof` a pointer parameter — pass the size explicitly across function boundaries.
  4. Enable `_FORTIFY_SOURCE` and the stack protector in release builds, and ASan/UBSan in CI.
  5. Add a regression test with an input exactly at, and one byte over, the buffer bound.

Fixes we shipped

Each of these is a pull request Orbis AppSec opened against a real open-source repository.

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.

How insecure-use-string-copy-fn happens in C and how to fix it

A high-severity vulnerability was identified in `plugin/bin/install.c` where `strcpy()` and `strncpy()` were used to handle path strings without proper bounds checking or guaranteed null-termination. The fix replaces `strcpy()` with direct character assignment and `strncpy()` with `snprintf()`, eliminating both buffer overflow and missing null-terminator risks in the plugin installation workflow.

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

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

A high-severity buffer overflow vulnerability was discovered in `tools/claude-vscode-wrapper.c`, where an unbounded `strcpy()` call copied a file path into a fixed-size `MAX_PATH` buffer without any size validation. The fix replaces `strcpy()` with `snprintf()` and swaps `malloc()` for `calloc()`, ensuring both string operations and memory allocation are bounds-safe and zero-initialized.

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

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.

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

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.

Browse every buffer overflow case study

Frequently asked questions

Is strncpy a safe replacement for strcpy?

No. `strncpy` copies at most n bytes but does not append a NUL when the source is n bytes or longer, so the destination is left unterminated and the next `strlen` or `printf("%s")` reads past the buffer. It also pads short sources with NUL bytes up to n, which makes it slow for large buffers. Use `snprintf` or `strlcpy`, both of which always terminate.

Are the _s functions (strcpy_s, sprintf_s) the right answer?

They are safe when available, but Annex K of C11 is optional and glibc has never implemented it, so portable code cannot rely on them. On MSVC they are the natural choice. Elsewhere `snprintf` is in every C99 implementation and `strlcpy` is in glibc 2.38+, musl, and the BSDs.

Does -D_FORTIFY_SOURCE fix these calls?

It turns some of them into aborts instead of memory corruption, which converts a possible code-execution bug into a denial of service. That is a real improvement and worth enabling everywhere, but it only covers cases where the compiler can see the destination's size, and an abort in production is still an outage. Fix the call.

Is truncation safe if the buffer is bounded?

It depends on what the string is for. A truncated log line is cosmetic. A truncated file path can resolve to a different file than the one that was checked, and a truncated hostname or permission string can pass a comparison it should have failed. Treat truncation as an error unless you can state why the shorter value is equivalent.

Let Orbis AppSec find these for you

Orbis AppSec scans your GitHub repositories, traces the taint from source to sink, and opens a pull request with the fix applied and verified.

Try Orbis AppSec

Authoritative sources

See also: Buffer overflow fixes we shipped