Back to Blog
high SEVERITY8 min read

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.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This vulnerability is an insecure string copy (CWE-120, "Buffer Copy without Checking Size of Input") in a C-language VSCode wrapper tool. The function `strcpy(preload_url, preload)` at line 36 of `tools/claude-vscode-wrapper.c` copied a file-system path into a fixed `MAX_PATH` buffer with no length check, creating a classic stack buffer overflow primitive. The fix replaces `strcpy()` with `snprintf(preload_url, sizeof(preload_url), "%s", preload)`, which enforces the destination buffer size, and replaces `malloc()` with `calloc()` to zero-initialize the argument array, eliminating both the overflow and an uninitialized-memory risk.

Vulnerability at a Glance

cweCWE-120
fixReplace strcpy() with snprintf() using sizeof(preload_url) as the bound
riskStack buffer overflow leading to program crash or arbitrary code execution
languageC
root causestrcpy() copies preload path into MAX_PATH buffer with no length validation
vulnerabilityInsecure String Copy (strcpy without bounds checking)

The Problem with strcpy() in a VSCode Wrapper Tool

The tools/claude-vscode-wrapper.c file serves as a thin native launcher that bridges the VS Code extension for Claude Code to the underlying Node.js CLI. It constructs file-system paths from the Windows %APPDATA% environment variable, builds an argument vector, and hands control to node. It sounds simple — but a single strcpy() call at line 36 introduced a classic stack buffer overflow primitive that could be chained with other weaknesses by automated exploit tooling.

This post walks through exactly what went wrong, how the fix works, and what every C developer should take away from this real-world example.


The Vulnerability Explained

The Vulnerable Code

At line 36 of tools/claude-vscode-wrapper.c, the original code looked like this:

char preload_url[MAX_PATH];
strcpy(preload_url, preload);          // ← vulnerable line
for (char *p = preload_url; *p; p++) {
    if (*p == '\\') *p = '/';
}

preload is built earlier via snprintf() from the %APPDATA% environment variable:

snprintf(preload, sizeof(preload),
    "%s\\npm\\node_modules\\claude-code-cache-fix\\preload.mjs", appdata);

So the source string is controlled, at least partially, by the environment variable APPDATA. MAX_PATH on Windows is 260 characters — but APPDATA can be set to an arbitrarily long value by the user or by a malicious process that has already compromised the environment.

Why strcpy() Is Dangerous Here

strcpy(dst, src) has no concept of the destination buffer's size. It copies bytes from src until it hits a null terminator, regardless of how many bytes dst can hold. If preload exceeds MAX_PATH - 1 bytes, strcpy() will happily write past the end of the stack-allocated preload_url array.

The consequences of overflowing a stack buffer include:

  • Overwriting the saved return address, redirecting execution to attacker-controlled code.
  • Overwriting adjacent local variables, corrupting program state in unpredictable ways.
  • Program crash (denial of service), which is the most likely outcome in a modern hardened OS environment.

The Secondary Issue: malloc() Instead of calloc()

A second related issue appeared a few lines later:

char **new_argv = malloc(sizeof(char *) * (argc + 2));

malloc() returns uninitialized memory. If any pointer slot in new_argv is never explicitly assigned before use (e.g., due to an off-by-one in the loop that populates it), the program could pass a garbage pointer to execv() or equivalent, leading to undefined behavior. Using calloc() zero-initializes every slot, eliminating this risk.

Attack Scenario

Consider a developer or CI system that sets a deeply nested APPDATA path:

APPDATA=C:\Users\VeryLongUserNameThatExceedsNormalBounds\AppData\Roaming\<...260+ chars...>

When the wrapper runs, preload is constructed from this path and then strcpy(preload_url, preload) overflows the 260-byte stack buffer. On a system without stack canaries or where the canary is bypassed, this gives an attacker control over the instruction pointer. Even with modern mitigations, it reliably crashes the wrapper, preventing the VS Code extension from launching — a denial-of-service against developer tooling.


The Fix

The pull request harden: use bounded strlcpy/snprintf in claude-vscode-wrapper.c... makes two targeted changes.

Change 1: Replace strcpy() with snprintf()

- strcpy(preload_url, preload);
+ snprintf(preload_url, sizeof(preload_url), "%s", preload);

snprintf() takes an explicit maximum byte count as its second argument. It will write at most sizeof(preload_url) - 1 characters and always null-terminates the result (as long as the size argument is greater than zero). This single change eliminates the buffer overflow.

Why snprintf() with "%s" rather than strlcpy() or strcpy_s()?

  • strlcpy() is available on BSD and macOS but is not part of the C standard and is absent from many Linux and Windows toolchains without additional libraries.
  • strcpy_s() is defined in C11 Annex K but is marked optional and is not universally implemented (notably absent from glibc).
  • snprintf() is part of C99 and C11, available everywhere, and has well-defined truncation behavior.

Using snprintf(dst, sizeof(dst), "%s", src) is the most portable safe-copy idiom in standard C.

Change 2: Replace malloc() with calloc()

- char **new_argv = malloc(sizeof(char *) * (argc + 2));
+ char **new_argv = calloc((size_t)argc + 2, sizeof(char *));

calloc(nmemb, size) zero-initializes all allocated memory. This means every pointer in new_argv starts as NULL, so any slot that is accidentally skipped during population will be a null pointer rather than a garbage address. The explicit cast to (size_t) also prevents a potential integer overflow if argc were ever negative or unexpectedly large (though argc is non-negative by the C standard, the cast makes the intent explicit).

Before and After: Full Context

Before:

char preload_url[MAX_PATH];
strcpy(preload_url, preload);          // no bounds check
for (char *p = preload_url; *p; p++) {
    if (*p == '\\') *p = '/';
}

// ... later ...

char **new_argv = malloc(sizeof(char *) * (argc + 2));  // uninitialized
if (!new_argv) return 1;

After:

char preload_url[MAX_PATH];
snprintf(preload_url, sizeof(preload_url), "%s", preload);  // bounded
for (char *p = preload_url; *p; p++) {
    if (*p == '\\') *p = '/';
}

// ... later ...

char **new_argv = calloc((size_t)argc + 2, sizeof(char *));  // zero-initialized
if (!new_argv) return 1;

The logic of the wrapper — path construction, backslash-to-slash conversion, argument passing — is completely unchanged. Only the safety properties of the memory operations are improved.


Prevention & Best Practices

Never Use strcpy() in New Code

There is no safe use of strcpy() with external or environment-derived input. Modern C compilers and linters will warn about it; treat those warnings as errors.

Safe String Copy Alternatives

Function Standard Null-terminates? Bounds-checked? Notes
snprintf(dst, n, "%s", src) C99/C11 ✅ Always ✅ Yes Most portable
strlcpy(dst, src, n) BSD/POSIX ✅ Always ✅ Yes Not in glibc by default
strcpy_s(dst, n, src) C11 Annex K ✅ Always ✅ Yes Optional, not in glibc
strncpy(dst, src, n) C89/C99 ❌ Not guaranteed Partial Avoid — pads with nulls, doesn't terminate

Use calloc() Over malloc() for Pointer Arrays

When allocating arrays of pointers (like argument vectors), calloc() ensures all pointers start as NULL. This prevents accidental use of uninitialized pointers and makes the allocation intent clearer.

Enable Compiler and Linker Hardening

# GCC / Clang
-D_FORTIFY_SOURCE=2   # enables runtime bounds checking for string functions
-fstack-protector-strong  # adds stack canaries
-Wall -Wextra         # enables strcpy/strncpy warnings

Integrate Static Analysis in CI

Semgrep's c.lang.security.insecure-use-string-copy-fn rule detected this issue automatically. Add it to your CI pipeline:

# .github/workflows/semgrep.yml
- uses: semgrep/semgrep-action@v1
  with:
    config: p/c

CWE and OWASP References


Key Takeaways

  • strcpy(preload_url, preload) in claude-vscode-wrapper.c was a textbook stack buffer overflow: the destination was a fixed MAX_PATH (260-byte) stack array, and the source was derived from the user-controlled APPDATA environment variable.
  • snprintf(dst, sizeof(dst), "%s", src) is the most portable safe-copy idiom in standard C — it works on every C99-compliant toolchain, unlike strlcpy() or strcpy_s().
  • calloc() should be preferred over malloc() for pointer arrays: zero-initialization ensures that unused slots are NULL rather than garbage addresses.
  • Environment variables are attacker-controlled input: any path built from getenv() must be treated as untrusted and handled with bounded operations.
  • Static analysis tools like Semgrep can catch every strcpy() call automatically: adding the p/c ruleset to CI would have flagged this before it ever reached production.

How Orbis AppSec Detected This

  • Source: The APPDATA environment variable, read via getenv("APPDATA") and embedded into the preload string.
  • Sink: strcpy(preload_url, preload) at line 36 of tools/claude-vscode-wrapper.c, copying the environment-derived path into a fixed 260-byte stack buffer.
  • Missing control: No length check between the source string length and sizeof(preload_url) before the copy operation.
  • CWE: CWE-120 — Buffer Copy without Checking Size of Input ('Classic Buffer Overflow').
  • Fix: Replaced strcpy(preload_url, preload) with snprintf(preload_url, sizeof(preload_url), "%s", preload), enforcing the destination buffer's size on every copy.

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

A single strcpy() call in a developer tool wrapper is easy to overlook — it's a small file, a simple operation, and the path being copied looks reasonable at first glance. But because the source string is derived from an environment variable, it is attacker-influenced input, and without a bounds check, it is a genuine buffer overflow primitive.

The fix is minimal and surgical: snprintf() with sizeof(preload_url) as the bound, and calloc() instead of malloc() for the argument vector. These two lines of change eliminate both the overflow risk and the uninitialized-memory risk without altering any observable behavior for valid inputs.

The broader lesson is that every string copy in C that involves external data — environment variables, command-line arguments, file paths, network input — must use a bounded function. Make snprintf() your default, enable -D_FORTIFY_SOURCE=2 in your build flags, and add a Semgrep C ruleset to your CI pipeline. These are low-cost, high-value defenses that catch this entire class of vulnerability before it ships.


References

Frequently Asked Questions

What is an insecure string copy vulnerability in C?

It occurs when strcpy() or strncpy() is used to copy data into a fixed-size buffer without verifying the source fits, potentially overwriting adjacent memory and enabling buffer overflow attacks.

How do you prevent insecure string copy in C?

Use snprintf() with an explicit buffer size, or platform-specific safe alternatives like strlcpy() (BSD/macOS) or strcpy_s() (C11 Annex K), always passing the destination buffer's sizeof() value.

What CWE is insecure string copy?

CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow'), and related CWE-121 (Stack-based Buffer Overflow) when the destination is a stack-allocated array.

Is strncpy() enough to prevent buffer overflow in C?

No. strncpy() will not null-terminate the destination string if the source is longer than the specified length, which can cause subsequent string operations to read past the buffer boundary.

Can static analysis detect insecure string copy?

Yes. Tools like Semgrep, Coverity, and CodeQL have rules that flag every use of strcpy() and unsafe strncpy() patterns, making this class of vulnerability reliably detectable in CI pipelines.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #294

Related Articles

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.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left this Node.js library without a cooldown period on dependency updates, meaning Dependabot could immediately propose upgrades to newly published — potentially malicious or unstable — package versions. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, introducing a mandatory waiting period before any newly released version is surfaced as an update candidate. Because this project