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
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-121: Stack-based Buffer Overflow
- OWASP: Buffer Overflow Prevention Cheat Sheet
Key Takeaways
strcpy(preload_url, preload)inclaude-vscode-wrapper.cwas a textbook stack buffer overflow: the destination was a fixedMAX_PATH(260-byte) stack array, and the source was derived from the user-controlledAPPDATAenvironment variable.snprintf(dst, sizeof(dst), "%s", src)is the most portable safe-copy idiom in standard C — it works on every C99-compliant toolchain, unlikestrlcpy()orstrcpy_s().calloc()should be preferred overmalloc()for pointer arrays: zero-initialization ensures that unused slots areNULLrather 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 thep/cruleset to CI would have flagged this before it ever reached production.
How Orbis AppSec Detected This
- Source: The
APPDATAenvironment variable, read viagetenv("APPDATA")and embedded into thepreloadstring. - Sink:
strcpy(preload_url, preload)at line 36 oftools/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)withsnprintf(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.