Back to Blog
critical SEVERITY5 min read

How buffer overflow via sprintf() happens in C++ settings parsing and how to fix it

A critical buffer overflow vulnerability was discovered in `app/src/main/cpp/samp/settings.cpp` where `sprintf()` writes to a fixed 127-byte buffer (`char buff[0x7F]`) without bounds checking. If the `g_pszStorage` global variable contains a string longer than ~107 bytes, the formatted output exceeds the buffer, enabling stack corruption. The fix replaces `sprintf()` with `snprintf()` using `sizeof(buff)` to guarantee writes never exceed the declared buffer length.

O
By Orbis AppSec
Published June 28, 2026Reviewed June 28, 2026

Answer Summary

This is a CWE-120 buffer overflow vulnerability in C++ caused by using `sprintf()` on a fixed-size 127-byte buffer without bounds checking. The `g_pszStorage` global variable is concatenated with a hardcoded suffix via `sprintf(buff, "%sSAMP/settings.ini", g_pszStorage)`, and if the combined output exceeds 127 bytes, stack corruption occurs. The fix replaces `sprintf()` with `snprintf(buff, sizeof(buff), ...)` to enforce the buffer boundary.

Vulnerability at a Glance

cweCWE-120
fixReplace sprintf() with snprintf(buff, sizeof(buff), ...) at both call sites
riskStack-based buffer overflow enabling arbitrary code execution
languageC++
root causesprintf() writes to a 127-byte buffer without length enforcement
vulnerabilityBuffer overflow via unbounded sprintf()

How buffer overflow via sprintf() happens in C++ settings parsing and how to fix it

Introduction

In app/src/main/cpp/samp/settings.cpp, a critical buffer overflow vulnerability was discovered at line 15 inside the CSettings::CSettings() constructor. The code declares a 127-byte stack buffer (char buff[0x7F]) and then uses sprintf() to format a file path into it—concatenating the global g_pszStorage pointer with the hardcoded suffix "SAMP/settings.ini". Because sprintf() performs no bounds checking, an attacker who can influence g_pszStorage (through memory corruption, initialization manipulation, or controlled storage paths) can overflow the buffer, corrupt the stack, and potentially achieve arbitrary code execution.

This pattern is deceptively common in C/C++ codebases—especially in Android NDK projects where native code handles file path construction. The suffix "SAMP/settings.ini" alone consumes 18 bytes plus a null terminator (19 bytes total), meaning any g_pszStorage value longer than 107 characters will overflow the 127-byte buffer.


The Vulnerability Explained

Here is the vulnerable code from settings.cpp:

CSettings::CSettings()
{
    FLog("Loading settings..");

    char buff[0x7F];
    sprintf(buff, "%sSAMP/settings.ini", g_pszStorage);

    INIReader reader(buff);
    // ...

    // client
    size_t length = 0;
    sprintf(buff, "__android_%d%d", rand() % 1000, rand() % 1000);
    // ...
}

Why this is dangerous:

  1. Fixed-size buffer: char buff[0x7F] allocates exactly 127 bytes on the stack.
  2. Unbounded write: sprintf() will write as many bytes as the format string produces—there is no mechanism to stop at 127 bytes.
  3. External input in the format: g_pszStorage is a global pointer set during application initialization. Its value derives from the Android storage path, which could be manipulated.

Exploitation scenario:

Consider an attacker who can influence g_pszStorage—for example, through a prior memory corruption vulnerability, a malicious intent that sets the storage directory, or by exploiting a race condition during initialization:

g_pszStorage = "/data/data/com.example.app/files/AAAAAAAAAAAA...AAAA/"  // 120+ chars

When sprintf(buff, "%sSAMP/settings.ini", g_pszStorage) executes:
- The output would be 120 + 18 + 1 (null) = 139 bytes
- The buffer only holds 127 bytes
- The remaining 12 bytes overwrite adjacent stack memory

This stack corruption can overwrite:
- The saved return address → arbitrary code execution
- Local variables → logic manipulation
- Stack canaries (if present) → crash/denial of service

The second sprintf() call at line 28 (sprintf(buff, "__android_%d%d", rand() % 1000, rand() % 1000)) is less exploitable since rand() % 1000 produces at most 3 digits, making the maximum output "__android_999999" (16 bytes). However, it still uses an unsafe pattern that should be corrected for defense-in-depth.


The Fix

The fix replaces both sprintf() calls with snprintf(), passing sizeof(buff) as the maximum number of bytes to write (including the null terminator):

Before:

char buff[0x7F];
sprintf(buff, "%sSAMP/settings.ini", g_pszStorage);

After:

char buff[0x7F];
snprintf(buff, sizeof(buff), "%sSAMP/settings.ini", g_pszStorage);

Before (line 28):

sprintf(buff, "__android_%d%d", rand() % 1000, rand() % 1000);

After (line 28):

snprintf(buff, sizeof(buff), "__android_%d%d", rand() % 1000, rand() % 1000);

How this solves the problem:

snprintf(buff, sizeof(buff), ...) guarantees that at most sizeof(buff) - 1 characters are written, followed by a null terminator. If the formatted string would exceed 127 bytes, it is truncated rather than overflowing into adjacent memory.

  • sizeof(buff) evaluates to 0x7F (127) at compile time
  • snprintf returns the number of characters that would have been written (useful for detecting truncation)
  • The buffer is always null-terminated within its declared bounds

The security invariant is now enforced: buffer writes never exceed the declared length of 127 bytes, regardless of the content of g_pszStorage.

Trade-off: If g_pszStorage is too long, the path will be truncated and INIReader will fail to open the file. This is a safe failure mode—the application reports an error rather than executing attacker-controlled code.


Prevention & Best Practices

  1. Never use sprintf() with external or variable-length input. Always prefer snprintf() in C or std::string/std::format in C++.

  2. Use sizeof() rather than magic numbers. Writing snprintf(buff, sizeof(buff), ...) ensures the limit stays correct even if the buffer size changes later.

  3. Enable compiler warnings. Both GCC and Clang support -Wformat-overflow which can detect some sprintf() overflows at compile time.

  4. Enable stack protectors. Compile with -fstack-protector-strong to detect stack buffer overflows at runtime (though this is a mitigation, not a fix).

  5. Use static analysis tools. Semgrep, clang-tidy (bugprone-not-null-terminated-result), and Coverity can all flag sprintf() usage on fixed-size buffers.

  6. Consider using C++ string types. For path construction, std::string path = std::string(g_pszStorage) + "SAMP/settings.ini"; eliminates buffer management entirely.

  7. Audit all sprintf() calls. If one exists in a codebase, there are likely more. The PR notes that line 28 also needed the same fix.


Key Takeaways

  • sprintf() on a char buff[0x7F] with g_pszStorage concatenation is exploitable when the storage path exceeds 107 bytes—the 18-byte suffix "SAMP/settings.ini" plus null terminator leaves minimal headroom.
  • Both sprintf() calls in CSettings::CSettings() needed fixing—even the second one at line 28 that formats random numbers, because unsafe patterns should never remain in security-sensitive code.
  • snprintf(buff, sizeof(buff), ...) is a drop-in replacement that preserves the existing logic while enforcing the buffer boundary at the cost of truncation on overflow.
  • Android NDK native code is particularly vulnerable because storage paths vary by device and can be longer on custom ROMs or rooted devices.
  • A truncated path that fails to open is always preferable to a buffer overflow—fail safely, not catastrophically.

How Orbis AppSec Detected This

  • Source: The g_pszStorage global variable, populated during application initialization with the Android storage directory path
  • Sink: sprintf(buff, "%sSAMP/settings.ini", g_pszStorage) at app/src/main/cpp/samp/settings.cpp:15
  • Missing control: No bounds checking between the variable-length source (g_pszStorage) and the fixed-size destination (char buff[0x7F])
  • CWE: CWE-120 — Buffer Copy without Checking Size of Input
  • Fix: Replaced sprintf() with snprintf(buff, sizeof(buff), ...) to enforce the 127-byte buffer limit

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 via sprintf() remain one of the most dangerous and prevalent vulnerability classes in C/C++ code. In this case, a 127-byte stack buffer and an unbounded string format created a directly exploitable condition in production mobile application code. The fix—switching to snprintf() with sizeof(buff)—is minimal, backward-compatible, and definitively eliminates the overflow. If your codebase contains any sprintf() calls targeting fixed-size buffers, audit them now. The cost of the fix is negligible; the cost of exploitation is not.


References

Frequently Asked Questions

What is a buffer overflow via sprintf()?

A buffer overflow occurs when sprintf() formats a string that exceeds the destination buffer's allocated size, writing past its boundary and corrupting adjacent memory on the stack or heap.

How do you prevent buffer overflow in C++?

Use bounded alternatives like snprintf() that accept a maximum length parameter, or use std::string and C++ string formatting facilities that handle memory allocation automatically.

What CWE is buffer overflow?

CWE-120 (Buffer Copy without Checking Size of Input) covers cases where data is copied to a buffer without verifying the source data fits within the destination's allocated space.

Is using a large buffer enough to prevent buffer overflow?

No. Increasing buffer size only raises the threshold for exploitation—it doesn't eliminate the vulnerability. Bounds-checked functions like snprintf() are required to guarantee safety regardless of input size.

Can static analysis detect sprintf buffer overflow?

Yes. Static analyzers and linters (e.g., Semgrep, Coverity, clang-tidy) can flag sprintf() usage on fixed-size buffers as potential overflow risks, especially when format string arguments include external data.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #26

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.