Introduction
The file plugin/bin/install.c handles the plugin installation workflow—resolving the current working directory, finding the project root, and orchestrating a multi-step install process. But two string copy calls on lines 64 and 75 introduced subtle yet high-severity risks: an unbounded strcpy() in the get_root_dir() function and a strncpy() in main() that could silently drop the null terminator.
These aren't theoretical concerns. The root_path and path buffers are used immediately afterward in file existence checks, directory traversal, and printf() calls. A missing null terminator or an overflowed buffer in this context could corrupt adjacent stack variables, crash the installer, or—in a worst-case scenario—allow an attacker who controls the working directory path to inject code.
This post dissects exactly what went wrong, how the fix works at the code level, and why the chosen replacement functions are superior to the alternatives.
The Vulnerability Explained
Two Dangerous Patterns in One File
Pattern 1: Unbounded strcpy() at line 64
if (strlen(path) == 0) strcpy(path, ".");
Inside get_root_dir(), if no root directory marker is found while traversing upward, the function falls back to setting path to "." (the current directory). The strcpy() call here doesn't check the size of path. While the source string "." is only 2 bytes (including the null terminator), the real danger is the pattern: this teaches developers that strcpy() is acceptable in this codebase, and future modifications might introduce longer source strings without adding bounds checks.
More critically, static analysis tools like Semgrep flag this pattern because strcpy() is universally considered unsafe in security-sensitive code. Automated exploit-development tools scan for exactly these primitives as potential chain links.
Pattern 2: strncpy() without null-termination guarantee at line 75
strncpy(root_path, cwd, PATH_MAX);
In main(), the current working directory (cwd, obtained from getcwd()) is copied into root_path. While strncpy() limits the copy to PATH_MAX bytes, it has a critical flaw: if cwd is exactly PATH_MAX bytes long (including or excluding the null terminator, depending on getcwd()'s behavior), strncpy() will not append a null terminator to root_path.
This means every subsequent operation on root_path—the get_root_dir() call, the printf() on the next line, the snprintf() calls that build file paths—would read past the buffer boundary, causing undefined behavior.
Attack Scenario
Consider an attacker who can influence the working directory path (e.g., via a symlink chain or a crafted directory name on a shared system). If they create a directory path that is exactly PATH_MAX - 1 characters long:
getcwd()fillscwdwithPATH_MAXbytes (path + null terminator).strncpy(root_path, cwd, PATH_MAX)copies allPATH_MAXbytes but may not null-terminate ifcwduses the full buffer.get_root_dir(root_path)callsstrlen(root_path), which scans past the buffer looking for a null byte.- This out-of-bounds read could leak stack data, crash the program, or be chained with other vulnerabilities for code execution.
Even without an active attacker, edge-case paths on deeply nested filesystems could trigger this organically.
The Fix
The PR applies two targeted, minimal changes that preserve the original behavior while eliminating both vulnerability classes.
Change 1: Replace strcpy() with direct character assignment (line 64)
Before:
if (strlen(path) == 0) strcpy(path, ".");
After:
if (strlen(path) == 0) { path[0] = '.'; path[1] = '\0'; }
This is elegant in its simplicity. Since the source string is a single-character literal ".", there's no need for a string copy function at all. Direct assignment:
- Eliminates any function call overhead
- Makes the buffer write explicit and auditable (exactly 2 bytes)
- Removes the strcpy() from the codebase entirely, so static analyzers no longer flag it
- Guarantees null-termination by explicitly writing '\0'
Change 2: Replace strncpy() with snprintf() (line 75)
Before:
strncpy(root_path, cwd, PATH_MAX);
After:
snprintf(root_path, PATH_MAX, "%s", cwd);
snprintf() is the gold standard for bounded string operations in C because it provides both guarantees that strncpy() lacks:
- Bounds checking: It will never write more than
PATH_MAXbytes (including the null terminator). - Guaranteed null-termination: Even if truncation occurs, the output is always a valid C string.
The format string "%s" simply copies the source string, making this a drop-in replacement for strncpy() with strictly better safety properties. If cwd exceeds PATH_MAX - 1 characters, snprintf() truncates and null-terminates, whereas strncpy() would have left a non-terminated buffer.
Why Not strcpy_s()?
The Semgrep rule suggests strcpy_s() as an alternative, but the PR wisely avoids it. strcpy_s() is part of C11's optional Annex K (bounds-checking interfaces), which is not implemented by glibc, musl, or most Linux C libraries. Using snprintf() ensures portability across all POSIX-compliant systems without requiring special compiler flags or library support.
Prevention & Best Practices
1. Ban strcpy() and strncpy() from your codebase
Add a linting rule or pre-commit hook that flags any use of strcpy() or strncpy(). Both functions have safer alternatives:
| Unsafe Function | Safe Replacement | Why |
|---|---|---|
strcpy() |
snprintf() or strlcpy() |
Bounded copy with null-termination |
strncpy() |
snprintf() or strlcpy() |
Guaranteed null-termination |
sprintf() |
snprintf() |
Bounded formatting |
2. Prefer snprintf() for string copies in C
// Instead of this:
strncpy(dest, src, sizeof(dest));
// Do this:
snprintf(dest, sizeof(dest), "%s", src);
3. Use sizeof() instead of magic numbers
When possible, use sizeof(dest) rather than a named constant like PATH_MAX to ensure the size always matches the actual buffer declaration.
4. Run static analysis in CI/CD
Tools like Semgrep can catch these patterns before they reach production. The rule c.lang.security.insecure-use-string-copy-fn.insecure-use-string-copy-fn specifically targets this class of vulnerability.
5. Consider compiler warnings
Enable -Wall -Wextra and -Wstringop-truncation (GCC 8+) to get compile-time warnings about strncpy() calls that may not null-terminate.
Key Takeaways
strcpy(path, ".")inget_root_dir()was an unnecessary function call—direct character assignment (path[0] = '.'; path[1] = '\0';) is safer, faster, and clearer for constant-length strings.strncpy(root_path, cwd, PATH_MAX)inmain()could produce a non-null-terminatedroot_pathwhen the working directory path approachesPATH_MAXlength, causing undefined behavior in all downstream string operations.snprintf()is the portable, safe replacement for bothstrcpy()andstrncpy()in C—it bounds-checks and null-terminates, unlikestrncpy()which only does the former.- Even "safe-looking" code like copying a 2-byte string with
strcpy()creates exploit primitives that automated attack tools can chain with other weaknesses. - The fix in
plugin/bin/install.cis behavior-preserving—valid inputs produce identical output, but edge cases and malicious inputs are now handled safely.
How Orbis AppSec Detected This
- Source: The current working directory string returned by
getcwd(cwd, PATH_MAX)inmain()at line 71 ofplugin/bin/install.c, which reflects the filesystem path and could be influenced by an attacker controlling the directory structure. - Sink:
strcpy(path, ".")at line 64 andstrncpy(root_path, cwd, PATH_MAX)at line 75 inplugin/bin/install.c—both unsafe string copy functions operating on path buffers. - Missing control: No bounds checking on the
strcpy()call and no null-termination guarantee on thestrncpy()call, leaving both buffers vulnerable to overflow or unterminated string conditions. - CWE: CWE-120 (Buffer Copy without Checking Size of Input)
- Fix: Replaced
strcpy()with direct character assignment for the constant string case and replacedstrncpy()withsnprintf()for bounded, null-terminated path copying.
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
Buffer overflows from unsafe string copy functions remain one of the most common and dangerous vulnerability classes in C. This case in plugin/bin/install.c demonstrates two distinct flavors of the problem—strcpy() without bounds checking and strncpy() without null-termination—both operating on file path buffers that flow through the entire plugin installation pipeline.
The fix is minimal but effective: direct character assignment for trivial constant strings and snprintf() for general-purpose bounded copies. These patterns should be your default in any C codebase. Run static analysis tools like Semgrep in your CI pipeline to catch these issues before they ship, and treat every strcpy()/strncpy() occurrence as a code smell that warrants immediate remediation.