Back to Blog
critical SEVERITY9 min read

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

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

Answer Summary

This is a classic C++ buffer overflow vulnerability (CWE-120) in `xmlParser.cpp`'s `toXMLString` function, where `_tcscpy()` was used to write XML escape sequences (`<`, `>`, `&`, `'`, `"`) into a destination buffer without bounds checking. An attacker who controls the XML input could overflow the buffer and potentially achieve arbitrary code execution. The fix replaces every `_tcscpy()` call with a size-bounded `memcpy()` that copies exactly the required number of bytes (e.g., `memcpy(dest, _T("<"), 4*sizeof(TCHAR))`), eliminating the overflow condition entirely.

Vulnerability at a Glance

cweCWE-120 (Buffer Copy without Checking Size of Input)
fixReplace _tcscpy() with memcpy() using exact byte counts for each escape sequence
riskArbitrary code execution or denial of service via crafted XML input
languageC++
root cause_tcscpy() writes XML escape sequences without verifying destination buffer capacity
vulnerabilityStack/heap buffer overflow via unbounded string copy

How Buffer Overflow via strcpy() Happens in C++ XML Parsers and How to Fix It

Introduction

The toXMLString function inside buildroot-external/package/libxmlparser/xmlParser.cpp has one job: convert special characters in XML content into their safe escape sequences. Characters like <, >, &, ', and " must become &lt;, &gt;, &amp;, &apos;, and &quot; respectively. It sounds straightforward — but the original implementation used _tcscpy() to write those escape sequences into a destination buffer, with no check whatsoever on how much space remained. That single oversight created a critical buffer overflow that could allow an attacker to execute arbitrary code by feeding the parser a carefully crafted XML document.

This post walks through exactly how the vulnerability works, what the fix looks like at the code level, and what C++ developers can take away to avoid the same mistake in their own parsers and string-handling code.


The Vulnerability Explained

The Dangerous Pattern in toXMLString

Here is the vulnerable section of xmlParser.cpp (around line 156–165 in the original file):

// VULNERABLE CODE (before fix)
LPTSTR toXMLString(LPTSTR dest, LPCTSTR source)
{
    while (*source)
    {
        switch (*source)
        {
        case '<' : _tcscpy(dest, _T("&lt;"  )); dest += 4; break;
        case '>' : _tcscpy(dest, _T("&gt;"  )); dest += 4; break;
        case '&' : _tcscpy(dest, _T("&amp;" )); dest += 5; break;
        case '\'': _tcscpy(dest, _T("&apos;")); dest += 6; break;
        case '"' : _tcscpy(dest, _T("&quot;")); dest += 6; break;
        // ... default case handles regular characters
        }
        source++;
    }
}

The problem is _tcscpy(). This is the TCHAR-generic version of strcpy() — it copies a null-terminated string to the destination pointer and stops only when it hits \0. It does not accept a maximum length argument. It does not check whether dest has enough room.

The function receives a pre-allocated dest buffer from the caller, sized based on some estimate of the output length. If an attacker can cause the XML input to generate more escape sequences than the caller anticipated — for example, by packing a huge number of & characters into an element value — the writes through _tcscpy() will march right past the end of the allocated buffer.

How an Attacker Exploits This

The threat model notes that this is a local CLI tool where exploitation requires control over command-line arguments or input files. That means the attack surface is:

  1. Malicious XML files passed as input — an attacker who can place or substitute an XML file that the tool processes (e.g., in a build pipeline, a shared directory, or via a supply-chain compromise of a configuration file) can trigger the overflow.
  2. Embedded XML in buildroot packages — because this library lives in buildroot-external/package/libxmlparser/, it processes XML data as part of the build system. A compromised or crafted package definition could supply the malicious input.

A concrete exploitation scenario:

<!-- Crafted XML: 10,000 ampersands in a single attribute value -->
<config value="&amp;&amp;&amp;&amp;... (×10000) ...&amp;" />

Each & character becomes &amp; (5 characters). If the caller allocated a buffer sized for, say, 1,024 characters of output, this input would cause toXMLString to write roughly 50,000 bytes — overflowing the buffer by ~49 KB. Depending on what lives adjacent to dest in memory, this could overwrite return addresses, function pointers, or other security-critical data, leading to arbitrary code execution.

Why _tcscpy() Is the Culprit

_tcscpy (and its siblings strcpy, wcscpy) are notorious in C/C++ security because:

  • They have no length parameter — there is no way to tell them "stop after N bytes."
  • They are unconditionally trusting — they will write as many bytes as the source string contains.
  • The C standard itself has deprecated strcpy in favor of strcpy_s in C11, and major compilers emit warnings when they see it.

The five calls in the switch statement each copy a string literal of known, fixed length (4–6 TCHAR units). The literals themselves are safe — the danger is that the destination pointer dest is advanced through a buffer that may not be large enough to hold the fully escaped output for a large or adversarial input.


The Fix

Replacing _tcscpy() with Size-Bounded memcpy()

The fix is surgical and precise. Every _tcscpy() call is replaced with a memcpy() that specifies exactly how many bytes to copy:

// FIXED CODE (after fix)
case '<' : memcpy(dest, _T("&lt;"),   4*sizeof(TCHAR)); dest += 4; break;
case '>' : memcpy(dest, _T("&gt;"),   4*sizeof(TCHAR)); dest += 4; break;
case '&' : memcpy(dest, _T("&amp;"),  5*sizeof(TCHAR)); dest += 5; break;
case '\'': memcpy(dest, _T("&apos;"), 6*sizeof(TCHAR)); dest += 6; break;
case '"' : memcpy(dest, _T("&quot;"), 6*sizeof(TCHAR)); dest += 6; break;

Before vs. After

Aspect Before (Vulnerable) After (Fixed)
Function _tcscpy(dest, literal) memcpy(dest, literal, N*sizeof(TCHAR))
Bounds check None Copies exactly N TCHAR units
Null terminator Written by _tcscpy past the data Not written (not needed mid-buffer)
Risk Buffer overflow if dest too small No overflow from these calls

Why memcpy() Is the Right Tool Here

memcpy() copies exactly the number of bytes you specify — no more, no less. Since the escape sequences are string literals with known, compile-time-fixed lengths (4, 4, 5, 6, and 6 TCHAR units respectively), the byte counts are constants baked into the source code. There is no runtime uncertainty.

The sizeof(TCHAR) multiplier is important for correctness in Unicode builds: on Windows, TCHAR can be 2 bytes wide (wchar_t), so 4*sizeof(TCHAR) correctly copies 8 bytes for the 4-character string &lt; in wide-character mode.

Supporting Header Additions

The fix also adds three #include directives that were previously missing:

#include <stdint.h>
#include <stddef.h>
#include <limits.h>

These headers provide size_t, ptrdiff_t, and integer limit constants — foundational types for safe size arithmetic in C/C++. Their absence was a latent risk: code that performs pointer arithmetic or size calculations without these types may silently use incorrect integer widths on some platforms.

Version Bump

The library version was incremented from 1.12 to 1.13 in both libxmlparser.mk and the file header comment. This is good practice: a version change signals to downstream consumers that the library has changed, enabling them to track the security fix in their dependency manifests.


Prevention & Best Practices

1. Ban strcpy and Its Variants in Code Review

Add a linting rule or compiler flag to flag any use of strcpy, wcscpy, _tcscpy, strcat, and sprintf (without n-variants). Most modern C/C++ projects use -Wdeprecated-declarations or a custom Semgrep rule for this.

2. Prefer Size-Bounded Alternatives

Unsafe Safe Alternative Notes
strcpy(d, s) strlcpy(d, s, sizeof(d)) BSD/macOS; use strncpy_s on MSVC
_tcscpy(d, s) memcpy(d, s, n*sizeof(TCHAR)) When length is known at compile time
sprintf(d, fmt, ...) snprintf(d, sizeof(d), fmt, ...) Always use n variant
strcat(d, s) strlcat(d, s, sizeof(d)) Or use std::string

3. Use std::string in C++ Where Possible

The entire class of buffer overflow in string handling largely disappears when you use std::string or std::wstring. The toXMLString function could be rewritten as:

std::string toXMLStringSafe(const std::string& source) {
    std::string result;
    result.reserve(source.size() * 6); // worst case: every char is &quot;
    for (char c : source) {
        switch (c) {
            case '<':  result += "&lt;";   break;
            case '>':  result += "&gt;";   break;
            case '&':  result += "&amp;";  break;
            case '\'': result += "&apos;"; break;
            case '"':  result += "&quot;"; break;
            default:   result += c;        break;
        }
    }
    return result;
}

No raw pointers, no manual buffer management, no overflow possible.

4. Enable Compiler Hardening Flags

For C/C++ projects, enable:
- -D_FORTIFY_SOURCE=2 (GCC/Clang): detects overflow in memcpy, strcpy at runtime
- -fstack-protector-strong: adds stack canaries to detect overflow before return
- -fsanitize=address (ASan): catches out-of-bounds writes during testing
- /GS (MSVC): equivalent stack protection

5. Apply Static Analysis in CI

Tools that catch this pattern automatically:
- Semgrep with the c.lang.security.insecure-use-strcpy-fn rule
- clang-analyzer (scan-build or clang-tidy with cppcoreguidelines-*)
- cppcheck with --enable=warning
- Coverity or CodeQL for deeper dataflow analysis

Relevant Standards

  • CWE-120: Buffer Copy without Checking Size of Input ("Classic Buffer Overflow")
  • CWE-787: Out-of-bounds Write
  • OWASP: Buffer Overflow
  • SEI CERT C Coding Standard: STR31-C — Guarantee that storage for strings has sufficient space for character data and the null terminator

Key Takeaways

  • _tcscpy() in toXMLString() was copying escape sequences with no knowledge of how much buffer space remained — a textbook CWE-120 waiting to be exploited.
  • The fix uses memcpy() with compile-time-constant byte counts — because the escape sequence lengths are fixed, memcpy(dest, _T("&lt;"), 4*sizeof(TCHAR)) is both safe and efficient.
  • sizeof(TCHAR) matters in XML parsers that support both narrow and wide characters — omitting it would silently under-copy in Unicode builds.
  • Missing <stdint.h>, <stddef.h>, and <limits.h> is a warning sign — code that does pointer arithmetic without these headers may use incorrect integer types on some platforms.
  • Buildroot external packages are production attack surface — even internal build tooling that processes XML from package definitions needs the same security rigor as user-facing code.

How Orbis AppSec Detected This

  • Source: XML input data (element names, attribute values, text content) supplied to toXMLString() in xmlParser.cpp
  • Sink: _tcscpy(dest, _T("&lt;")) and four similar calls at lines ~159–163 of xmlParser.cpp, writing into a caller-managed destination buffer
  • Missing control: No bounds check on dest before writing; no maximum-length parameter passed to the copy function; no assertion that the destination buffer has sufficient remaining capacity
  • CWE: CWE-120 — Buffer Copy without Checking Size of Input
  • Fix: Replaced all five _tcscpy() calls with memcpy() calls specifying exact byte counts (N*sizeof(TCHAR)), eliminating the possibility of writing beyond the intended copy length

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 strcpy() and its TCHAR-generic cousin _tcscpy() have been a source of critical vulnerabilities for decades — and they keep appearing in production code because the functions are so easy to reach for. In xmlParser.cpp, the toXMLString function was doing exactly what its name promised, but the mechanism it used to write XML escape sequences could be weaponized by anyone who could feed the parser a large enough input.

The fix is a model of precision: five _tcscpy() calls replaced with five memcpy() calls, each specifying exactly the number of TCHAR units being copied. No behavior change, no performance regression, and the overflow path is closed permanently. Pair that with the addition of the missing safety headers and a version bump to signal the change, and you have a clean, reviewable, auditable security patch.

For developers maintaining C or C++ code that handles string data — especially in parsers, serializers, and format converters — the lesson is clear: treat every strcpy as a bug waiting to be filed. The safe alternatives (memcpy with explicit sizes, strlcpy, snprintf, or better yet std::string) cost almost nothing and eliminate an entire class of critical vulnerabilities.


References

Frequently Asked Questions

What is a buffer overflow in C++ string handling?

A buffer overflow occurs when a function writes more data into a fixed-size memory buffer than it can hold, overwriting adjacent memory. In C++, functions like strcpy() and _tcscpy() copy strings until they hit a null terminator with no regard for the destination buffer's size, making them inherently unsafe with variable-length input.

How do you prevent buffer overflows in C++ XML parsers?

Use size-bounded alternatives: replace strcpy()/tcscpy() with memcpy() (specifying exact byte counts), strlcpy() (which enforces a maximum length), or snprintf() for formatted output. Always validate that the destination buffer is large enough before copying, and consider using C++ std::string or a bounds-checked string library.

What CWE is a buffer overflow from strcpy?

Unbounded string copies with strcpy() and related functions map to CWE-120 (Buffer Copy without Checking Size of Input), sometimes called a "Classic Buffer Overflow." When the input comes directly from an attacker, it may also be classified under CWE-787 (Out-of-bounds Write).

Is input validation alone enough to prevent buffer overflows from strcpy?

No. Input validation can reduce risk but is not sufficient on its own because validation logic can be bypassed or may miss edge cases. The only reliable fix is to replace unbounded copy functions with size-bounded alternatives like memcpy() with explicit sizes, strlcpy(), or snprintf(), so the buffer cannot overflow regardless of input length.

Can static analysis detect strcpy buffer overflows?

Yes. Static analysis tools such as Semgrep, Coverity, clang-analyzer, and cppcheck can flag uses of strcpy(), _tcscpy(), and similar functions as potentially unsafe. Orbis AppSec's multi-agent AI scanner detected this exact pattern in xmlParser.cpp and automatically generated the fix.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4056

Related Articles

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.

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.

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