Back to Blog
critical SEVERITY9 min read

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

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

Answer Summary

This is a heap buffer overflow vulnerability (CWE-120) in C++ ZIP extraction code (`TKLiveSync/unzip.cpp`). The root cause is a `strcpy()` call that copies untrusted ZIP entry names into a fixed `PATH_MAX`-sized heap buffer, while the ZIP spec allows names up to 65,535 bytes. The fix replaces `strcpy()` and `dirname()` with `std::string` operations — which manage memory dynamically — and adds an `is_safe_entry_name()` validator that rejects absolute paths, `..` components, and empty names before any string manipulation occurs.

Vulnerability at a Glance

cweCWE-120
fixReplace strcpy()/dirname() with std::string and add is_safe_entry_name() input validation
riskHeap corruption, potential remote code execution via crafted ZIP archive
languageC++
root causestrcpy() copies untrusted ZIP entry names into a PATH_MAX heap buffer without checking length
vulnerabilityHeap Buffer Overflow via Unbounded strcpy() on ZIP Entry Names

How Heap Buffer Overflows Happen in C++ ZIP Extraction and How to Fix Them

Summary

A critical heap buffer overflow was discovered and fixed in TKLiveSync/unzip.cpp, a production C++ file responsible for extracting ZIP archives in a Node.js native module. The vulnerable code used strcpy() to copy untrusted ZIP entry names into a PATH_MAX-sized heap buffer — a pattern that is guaranteed to overflow when processing a crafted archive with long filenames. The fix eliminates the unsafe buffer entirely, replaces dirname() with std::string operations, and introduces an entry-name validator that blocks both oversized names and path traversal attempts.


Introduction

The TKLiveSync/unzip.cpp file handles ZIP archive extraction for a Node.js native library. Its job is to iterate over every entry in a ZIP file, recreate the directory structure, and write file contents to a destination path. That sounds routine — but a flaw in how entry names were handled before directory creation created a textbook heap buffer overflow.

The offending pattern was on line 49, inside the unzip() function:

auto pathcopy = new char[PATH_MAX];
// ... later, inside the loop:
strcpy(pathcopy, name);
auto path = dirname(pathcopy);

pathcopy is allocated exactly PATH_MAX bytes on the heap — typically 4,096 bytes on Linux and 1,024 bytes on macOS. Then strcpy() copies name — the raw entry name from the ZIP archive — directly into that buffer with zero length checking.

The ZIP specification (PKWARE APPNOTE, section 4.4.17) allows entry names up to 65,535 bytes. Any attacker who can deliver a crafted ZIP file to a consumer of this library can trigger a heap overflow with a single entry whose name exceeds PATH_MAX.


The Vulnerability Explained

The Dangerous Code Path

Here is the vulnerable loop body as it existed before the fix:

auto pathcopy = new char[PATH_MAX];   // heap buffer, PATH_MAX bytes

for (zip_int64_t i = 0; i < num; i++) {
    zip_stat_index(z, i, ZIP_STAT_MTIME, &sb);
    auto name = sb.name;              // raw, untrusted ZIP entry name

    std::string assetFullname{ destination };
    assetFullname.append("/");
    assetFullname.append(name);

    strcpy(pathcopy, name);           // ← OVERFLOW HERE if len(name) >= PATH_MAX
    auto path = dirname(pathcopy);
    std::string dirFullname(destination);
    dirFullname.append("/");
    dirFullname.append(path);
    mkdir_rec(dirFullname.c_str());
    // ... file extraction continues
}

The problem is straightforward: strcpy() has no length parameter. It copies bytes from name into pathcopy until it hits a null terminator, regardless of how large pathcopy is. When name is 65,535 bytes (the ZIP spec maximum), strcpy() writes approximately 61,000 bytes past the end of a 4,096-byte heap allocation.

Why the ZIP Spec Makes This Worse

This isn't a theoretical edge case. The ZIP format stores filename length as a 16-bit unsigned integer in the local file header (offset 26), meaning any value from 0 to 65,535 is structurally valid. A malicious archive creator does not need to exploit any other vulnerability — they simply name a file with a 65,000-character string and package it into a .zip file. Tools like Python's zipfile module or raw binary construction make this trivial.

Exploitation Scenario

Consider this attack chain for a downstream Node.js application using this library:

  1. An attacker crafts a ZIP archive containing one entry with a filename of 65,535 A characters.
  2. The application receives this archive (via upload, download, or sync operation) and calls unzip().
  3. strcpy(pathcopy, name) writes 65,535 bytes into a 4,096-byte heap buffer.
  4. The overflow corrupts heap metadata and adjacent allocations.
  5. Depending on the heap layout, this can cause a crash (denial of service) or, with careful heap grooming, enable arbitrary code execution.

Because pathcopy is allocated with new char[PATH_MAX] and never freed inside the loop, there is also a secondary memory leak — but the overflow is the critical issue.

Additional Risk: No Path Traversal Validation

The original code also lacked any check for path traversal sequences. An entry named ../../etc/passwd would pass straight through to assetFullname and dirFullname, potentially writing files outside the intended destination directory. The original dirname() call would silently process the .. components.


The Fix

The fix addresses both problems — the buffer overflow and the path traversal risk — with two coordinated changes.

Change 1: The is_safe_entry_name() Validator

A new static helper function is added before unzip():

// ZIP entry names are untrusted input: reject absolute paths and ".."
// components so extraction can never write outside `destination`.
static bool is_safe_entry_name(const char* name)
{
    if (name == nullptr || *name == '\0' || *name == '/')
        return false;

    for (const char* p = name; *p;) {
        const char* component = p;
        while (*p && *p != '/')
            p++;
        if (p - component == 2 && component[0] == '.' && component[1] == '.')
            return false;
        if (*p == '/')
            p++;
    }

    return true;
}

This function enforces three rules before any memory operation occurs:

  • Null/empty rejection: nullptr and empty strings are invalid.
  • Absolute path rejection: Names starting with / are rejected, preventing writes to absolute filesystem paths.
  • .. component rejection: Each path component is checked character by character. Any component that is exactly .. causes the function to return false, blocking directory traversal.

The function is called at the top of the extraction loop, and entries that fail validation are simply skipped with continue — no memory is touched.

Change 2: Replacing strcpy/dirname with std::string

The pathcopy heap buffer and strcpy() call are removed entirely. Directory extraction now uses std::string::find_last_of():

Before:

auto pathcopy = new char[PATH_MAX];
// ...
strcpy(pathcopy, name);
auto path = dirname(pathcopy);
std::string dirFullname(destination);
dirFullname.append("/");
dirFullname.append(path);
mkdir_rec(dirFullname.c_str());

After:

std::string entryName{ name };
auto separator = entryName.find_last_of('/');

if (separator != std::string::npos) {
    std::string dirFullname{ destination };
    dirFullname.append("/");
    dirFullname.append(entryName.substr(0, separator));
    mkdir_rec(dirFullname.c_str());
}

std::string manages its own memory dynamically — it allocates exactly as much space as needed for the string content, regardless of length. There is no fixed-size buffer to overflow. The #include <libgen.h> header (which provided dirname()) is also removed, since it is no longer needed.

Why This Combination Works

The is_safe_entry_name() check runs before any string construction. By the time std::string entryName{ name } executes, the entry name has already been validated to be non-null, non-empty, non-absolute, and free of .. components. The std::string operations then handle arbitrary-length names safely because they manage heap allocation internally.


Prevention & Best Practices

1. Never Use strcpy() with Untrusted Input

strcpy() has been considered dangerous for decades. The POSIX standard itself notes that it "may overflow." In any C or C++ code that processes external data:

  • Replace strcpy() with std::string assignment or construction.
  • If raw buffers are required, use strlcpy() (BSD/macOS) or strncpy() with an explicit size and manual null termination.
  • Consider enabling compiler warnings: -Wdeprecated-declarations and -D_FORTIFY_SOURCE=2 on GCC/Clang will warn about or harden unsafe string functions.

2. Validate Archive Entry Names Before Processing

ZIP, TAR, and other archive formats are common attack vectors because their metadata (filenames, paths, permissions) is entirely attacker-controlled. Before processing any archive entry:

  • Reject absolute paths (names starting with / or a drive letter on Windows).
  • Reject .. components in any path segment.
  • Enforce a maximum name length appropriate to your platform.
  • Consider using a library like libarchive that has built-in path safety options.

3. Use RAII String Types in C++

std::string, std::filesystem::path, and similar RAII types eliminate an entire class of buffer overflow bugs by managing allocation automatically. Reserve raw char arrays for performance-critical inner loops where you control both the source and the maximum size.

4. Apply the ZIP Slip Checklist

The SNYK "Zip Slip" vulnerability class (which this code was also exposed to via the missing .. check) has a well-documented checklist:

  • Canonicalize the destination path with realpath() or std::filesystem::canonical().
  • After constructing the full output path, verify it still starts with the intended destination prefix.
  • Reject entries that resolve outside the destination.

5. Enable AddressSanitizer During Testing

Building with -fsanitize=address (ASAN) during development and CI will catch heap overflows at runtime during testing, often before they reach production. This specific overflow would have been caught immediately by ASAN on any test that processes a crafted ZIP.

Relevant Standards

  • CWE-120: Buffer Copy without Checking Size of Input ("Classic Buffer Overflow")
  • CWE-122: Heap-based Buffer Overflow
  • CWE-22: Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal")
  • OWASP: File Upload Cheat Sheet — archive extraction section

Key Takeaways

  • strcpy(pathcopy, name) in unzip.cpp was the single line that made this critical. A PATH_MAX-sized allocation is not a safe bound for ZIP entry names, which the spec allows up to 65,535 bytes.
  • ZIP entry names are untrusted input, always. The sb.name field in zip_stat comes directly from the archive file and must be validated before any memory operation — not after.
  • Replacing dirname() with std::string::find_last_of('/') eliminates both the overflow and the dependency on <libgen.h>. The fix is strictly simpler code, not just safer code.
  • Path traversal and buffer overflow often coexist in archive extraction code. The is_safe_entry_name() function addresses both in a single, readable validation pass.
  • new char[PATH_MAX] allocated outside a loop but used inside it is a memory-safety red flag. If the buffer cannot be proven to be large enough for every possible iteration input, it should be replaced with a dynamically sized type.

How Orbis AppSec Detected This

  • Source: The sb.name field returned by zip_stat_index() in unzip.cpp — raw, attacker-controlled ZIP entry name data read directly from the archive file.
  • Sink: strcpy(pathcopy, name) at line 49 of TKLiveSync/unzip.cpp, where the untrusted name is copied into the fixed PATH_MAX-sized heap buffer pathcopy.
  • Missing control: No length check was performed before the copy. The code did not compare strlen(name) against PATH_MAX, did not use a bounded copy function, and did not validate path components for traversal sequences.
  • CWE: CWE-120 — Buffer Copy without Checking Size of Input ("Classic Buffer Overflow").
  • Fix: The pathcopy buffer and strcpy() call were removed and replaced with std::string operations, and a new is_safe_entry_name() validator was added to reject unsafe entry names before any string manipulation.

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 ZIP extraction loop turned a routine file operation into a critical heap buffer overflow. The root cause was the mismatch between a PATH_MAX-sized allocation and the ZIP specification's 65,535-byte maximum entry name length — a gap large enough to drive a truck through. The fix is a model of defensive C++: validate untrusted input at the boundary with is_safe_entry_name(), then use std::string to eliminate fixed-size buffer concerns entirely.

If you maintain C or C++ code that processes archive files, treat every field in the archive metadata as hostile input. Validate lengths, reject traversal sequences, and prefer RAII string types over raw char arrays. These habits close an entire category of vulnerabilities before they ever reach a security scanner.


References

Frequently Asked Questions

What is a heap buffer overflow?

A heap buffer overflow occurs when a program writes more data into a heap-allocated buffer than it was sized to hold, corrupting adjacent memory and potentially enabling code execution or crashes.

How do you prevent buffer overflows in C++ string handling?

Use std::string or other RAII string types instead of raw char arrays and strcpy(). If raw buffers are unavoidable, always use bounded functions like strncpy() or strlcpy() and validate input length before copying.

What CWE is a buffer overflow?

Unbounded copy buffer overflows are classified as CWE-120 (Buffer Copy without Checking Size of Input). Heap-specific variants also relate to CWE-122 (Heap-based Buffer Overflow).

Is PATH_MAX a safe buffer size for ZIP entry names?

No. PATH_MAX is typically 1,024–4,096 bytes on common platforms, while the ZIP specification allows entry names up to 65,535 bytes. Using PATH_MAX as a buffer bound for ZIP entry names creates a guaranteed overflow condition on crafted archives.

Can static analysis detect this type of buffer overflow?

Yes. Tools like Semgrep, Coverity, and CodeQL have rules that flag strcpy() calls where the source is untrusted or unbounded. The multi_agent_ai scanner detected this exact pattern in unzip.cpp at line 49.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #429

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

high

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.

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.

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript