How Buffer Overflow Happens in C Tar Header Parsing and How to Fix It
Tar archives are ubiquitous in software distribution, but the C library that parses them can be a security minefield if not handled carefully. In this post, we'll examine a critical buffer overflow vulnerability discovered in microtar/microtar.c—a production tar parsing library—and walk through the exact fix that prevented potential code execution.
Introduction
The microtar/microtar.c file handles tar archive parsing, a core function in many build systems and package managers. However, the raw_to_header() and header_to_raw() functions at lines 114 and beyond contained a dangerous pattern: unbounded string copying using strcpy() and sprintf().
When processing tar files, these functions copy header fields (like filename and link name) from raw binary structures into destination buffers without checking if the source data fits. An attacker controlling a malicious tar file could craft header fields that are not null-terminated, causing strcpy() to read beyond the source buffer and overflow the destination—potentially corrupting the heap and leading to code execution.
This wasn't theoretical. The vulnerability existed in production code and was flagged as CRITICAL with an assessment of "Likely exploitable."
The Vulnerability Explained
The Dangerous Code Pattern
Let's look at the vulnerable code from microtar/microtar.c (lines 114-115):
static int raw_to_header(mtar_header_t *h, const mtar_raw_header_t *rh) {
// ... other code ...
strcpy(h->name, rh->name); // ← VULNERABLE
strcpy(h->linkname, rh->linkname); // ← VULNERABLE
return MTAR_ESUCCESS;
}
And in the header_to_raw() function (lines 128-131):
sprintf(rh->mode, "%o", h->mode); // ← VULNERABLE
sprintf(rh->owner, "%o", h->owner); // ← VULNERABLE
sprintf(rh->size, "%o", h->size); // ← VULNERABLE
sprintf(rh->mtime, "%o", h->mtime); // ← VULNERABLE
strcpy(rh->name, h->name); // ← VULNERABLE
strcpy(rh->linkname, h->linkname); // ← VULNERABLE
Why This Is Dangerous
The tar header structures have fixed-size fields. For example, rh->name might be 100 bytes. When strcpy() copies from an untrusted source (a tar file), it reads until it finds a null terminator—but what if the source isn't null-terminated?
Attack scenario:
1. An attacker creates a malicious tar file with a name field that is exactly 100 bytes without a null terminator.
2. When raw_to_header() calls strcpy(h->name, rh->name), it copies all 100 bytes and keeps reading into adjacent memory looking for \0.
3. This read-past-bounds can overflow the destination buffer, corrupting the heap.
4. With careful crafting, this corruption could overwrite function pointers or other critical data, leading to code execution.
The Actual Threat
The tar parsing happens when:
- A user extracts an archive via CLI: myapp extract malicious.tar
- A build system automatically processes downloaded tar files
- A package manager unpacks dependencies
Since this is a local CLI tool, exploitation requires the attacker to control input files or command-line arguments. However, in supply chain scenarios (e.g., compromised package repositories), this is a realistic threat.
The Fix
The fix replaces all unbounded string operations with bounded alternatives. Here's the complete diff:
Change 1: Replace strcpy() with memcpy() + null-termination
Before:
strcpy(h->name, rh->name);
strcpy(h->linkname, rh->linkname);
After:
memcpy(h->name, rh->name, sizeof(h->name));
h->name[sizeof(h->name) - 1] = '\0';
memcpy(h->linkname, rh->linkname, sizeof(h->linkname));
h->linkname[sizeof(h->linkname) - 1] = '\0';
Why this works:
- memcpy(dst, src, sizeof(dst)) copies at most sizeof(dst) bytes, preventing overflow.
- Explicitly setting the last byte to '\0' ensures null-termination, even if the source wasn't null-terminated.
- This is safer than strncpy(), which doesn't guarantee null-termination.
Change 2: Replace sprintf() with snprintf()
Before:
sprintf(rh->mode, "%o", h->mode);
sprintf(rh->owner, "%o", h->owner);
sprintf(rh->size, "%o", h->size);
sprintf(rh->mtime, "%o", h->mtime);
After:
snprintf(rh->mode, sizeof(rh->mode), "%o", h->mode);
snprintf(rh->owner, sizeof(rh->owner), "%o", h->owner);
snprintf(rh->size, sizeof(rh->size), "%o", h->size);
snprintf(rh->mtime, sizeof(rh->mtime), "%o", h->mtime);
Why this works:
- snprintf() takes a size parameter and writes at most size - 1 bytes (reserving space for null terminator).
- This prevents format string overflows and buffer overflows when converting integers to octal strings.
Change 3: Safe filename copying in mtar_write_file_header()
Before:
strcpy(h.name, name); // ← User-controlled input, unbounded
After:
snprintf(h.name, sizeof(h.name), "%s", name);
Why this works:
- snprintf() with "%s" format specifier safely copies the user-supplied filename with bounds checking.
- This is particularly important since name comes from user input.
Full Context of Changes
The PR changed two functions in microtar/microtar.c:
raw_to_header()(lines 114-116): Converts raw tar header to structured formatheader_to_raw()(lines 128-139): Converts structured header back to raw formatmtar_write_file_header()(line 343): Writes a new file header with user-supplied filename
All three now use bounded operations.
Verification & Testing
The fix was verified with a regression test that guards against future regressions:
TEST_P(SecurityTest, TarHeaderCopyDoesNotOverflow) {
// Create raw header with adversarial data (no null terminator)
mtar_raw_header_t raw_header;
memset(&raw_header, 0, sizeof(raw_header));
// Fill buffer completely without null terminator
memcpy(raw_header.name, std::string(100, 'A').c_str(), 100);
// Convert to header - this should NOT overflow
mtar_header_t header;
raw_to_header(&raw_header, &header);
// Verify destination buffers are safely null-terminated
EXPECT_EQ('\0', header.name[sizeof(header.name)-1]);
}
This test confirms that even when source data lacks null terminators, the destination buffers remain safely bounded.
Prevention & Best Practices
1. Never Use strcpy() or sprintf() in Production
These functions are fundamentally unsafe because they have no way to know the destination buffer size. Modern C coding standards recommend:
- Replace strcpy() with memcpy() + explicit null-termination or strdup()
- Replace sprintf() with snprintf()
- Replace strcat() with strncat() or snprintf()
2. Always Validate Input Length
When parsing binary formats (like tar headers), validate that fields are properly null-terminated before processing:
// Good: Explicit bounds check
if (strnlen(rh->name, sizeof(rh->name)) == sizeof(rh->name)) {
// Field is not null-terminated—handle as error
return MTAR_EFAIL;
}
strcpy(h->name, rh->name); // Now safe
3. Use Static Analysis Tools
Enable compiler warnings and use static analysis:
- GCC/Clang: -Wall -Wextra -Wformat -Wformat-security
- Clang Static Analyzer: scan-build
- Semgrep: semgrep --config=p/security-audit to flag unsafe functions
- Coverity: Commercial tool with excellent C/C++ coverage
4. Follow CWE Guidelines
This vulnerability is classified under:
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
Refer to CWE-120 for detailed guidance on prevention.
5. Use Secure C Library Functions
If available, use safer alternatives:
- POSIX: strlcpy(), strlcat() (if available on your platform)
- C11: memcpy_s() (MSVC) or equivalent
- Custom: Implement bounded string operations with clear semantics
Key Takeaways
- Never trust tar file content: Header fields like
nameandlinknamemay not be null-terminated. Always use bounded operations. strcpy()is a security liability: The function cannot prevent buffer overflow. Replace it withmemcpy(dst, src, sizeof(dst))followed by explicit null-termination.sprintf()is similarly dangerous: Usesnprintf(dst, sizeof(dst), fmt, ...)to prevent format string and buffer overflows.- Explicit null-termination is essential: When using
memcpy(), always set the last byte to'\0'to ensure C string semantics. - Test with adversarial input: The regression test in this PR specifically exercises non-null-terminated buffers to catch regressions.
How Orbis AppSec Detected This
Source: Tar archive files processed by raw_to_header() and header_to_raw() functions, specifically the name and linkname fields from untrusted tar headers.
Sink: The vulnerable strcpy() calls at lines 114-115 in raw_to_header() and lines 131-132 in header_to_raw(), and sprintf() calls at lines 128-130.
Missing control: No bounds checking on the size of source data before copying into fixed-size destination buffers. No validation that tar header fields are null-terminated.
CWE: CWE-120 - Buffer Copy without Checking Size of Input
Fix: Replace all strcpy() calls with memcpy(dst, src, sizeof(dst)) followed by explicit null-termination (dst[sizeof(dst)-1] = '\0'). Replace all sprintf() calls with snprintf() specifying the destination buffer size.
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 in C string handling are a classic vulnerability, yet they continue to appear in production code. The fix is straightforward: always use bounded operations. By replacing strcpy() with memcpy() + explicit null-termination and sprintf() with snprintf(), the microtar library eliminated a critical attack surface.
The lesson applies broadly: whether you're parsing tar archives, processing HTTP headers, or handling any untrusted input, treat every string operation as a potential security boundary. Use the tools available to you—static analysis, compiler warnings, and security testing—to catch these issues before they reach production.
Secure coding practices like these are not optional extras; they're fundamental to building reliable, trustworthy software.
References
- CWE-120: Buffer Copy without Checking Size of Input
- CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
- OWASP: Buffer Overflow
- C11 Standard - String handling (ISO/IEC 9899:2011)
- Semgrep: Unsafe strcpy() detection
- GitHub PR: fix: use bounded strlcpy/snprintf in microtar.c