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 Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.

critical

How Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.