Back to Blog
high SEVERITY6 min read

How buffer overflow via sprintf() happens in C string formatting and how to fix it

A high-severity buffer overflow vulnerability was discovered in `bench/strbuild/strbuild.c` where `sprintf()` wrote formatted output into a 64-byte stack buffer (`line[64]`) without any bounds checking. An attacker who could influence the values in the `NAMES[]`, `c[]`, or `v[]` arrays could overflow this buffer, potentially corrupting the stack and hijacking control flow. The fix replaces `sprintf()` with `snprintf(line, sizeof(line), ...)` to enforce a strict 64-byte write limit.

O
By Orbis AppSec
Published July 23, 2026Reviewed July 23, 2026

Answer Summary

This is a buffer overflow vulnerability (CWE-120) in C caused by using `sprintf()` to write formatted data into a fixed-size 64-byte stack buffer without bounds checking. The function `run_kernel()` in `bench/strbuild/strbuild.c` formatted integers and strings from arrays into `char line[64]` using `sprintf()`, which writes unlimited bytes. The fix replaces `sprintf(line, ...)` with `snprintf(line, sizeof(line), ...)` to guarantee the write never exceeds the buffer size.

Vulnerability at a Glance

cweCWE-120
fixReplace sprintf() with snprintf(line, sizeof(line), ...) to bound the write
riskStack buffer overflow leading to code execution or crash
languageC
root causesprintf() writes to a 64-byte buffer without enforcing size limits
vulnerabilityBuffer overflow via unbounded sprintf()

How buffer overflow via sprintf() happens in C string formatting and how to fix it

Introduction

In the bench/strbuild/strbuild.c file of a Node.js native library, we discovered a high-severity buffer overflow at line 42. The run_kernel() function formats benchmark data — an index, a name string, and a computed integer — into a 64-byte stack-allocated buffer called line[64] using sprintf(). Because sprintf() has no concept of a maximum write length, any combination of inputs that produces a formatted string longer than 63 characters (plus null terminator) would overflow the buffer, corrupting the stack frame.

This matters because the library is consumed by downstream Node.js packages. If any code path allows external influence over the NAMES[] array contents or the integer values passed to run_kernel(), an attacker could trigger a stack smash in the native addon — crashing the Node.js process or, worse, achieving arbitrary code execution.

The Vulnerability Explained

Here's the vulnerable code at line 42 of bench/strbuild/strbuild.c:

static uint32_t run_kernel(const int32_t* c, const int32_t* v) {
  char line[64];
  for (int it = 0; it < N_ITERS; it++) {
    for (int i = 0; i < N; i++) {
      int len = sprintf(line, "%d,%s,%d\n", i, NAMES[c[i]], (int)(v[i] + it));
      for (int j = 0; j < len; j++) h = mix_u32(h, (uint32_t)(uint8_t)line[j]);
    }
  }
}

The format string "%d,%s,%d\n" concatenates:
- An integer i (up to 10 digits for a 32-bit int)
- A comma
- A string NAMES[c[i]] (unbounded length)
- A comma
- An integer v[i] + it (up to 11 characters including sign)
- A newline

If NAMES[c[i]] is, say, 50 characters long and i is a multi-digit number, the total formatted output easily exceeds 64 bytes. sprintf() will happily write past the end of line[], overwriting:

  1. The saved frame pointer
  2. The return address
  3. Other local variables on the stack

Attack Scenario

Consider this concrete exploitation path:

  1. An attacker influences the NAMES[] array (perhaps through a configuration file, environment variable, or upstream data source that populates the benchmark names).
  2. They supply a name string of 60+ characters.
  3. When run_kernel() iterates and formats "%d,%s,%d\n" with this long name, sprintf() writes ~75 bytes into the 64-byte line buffer.
  4. The overflow overwrites the return address on the stack.
  5. When run_kernel() returns, execution jumps to an attacker-controlled address.

Even if exploitation for code execution is difficult due to stack canaries or ASLR, the overflow reliably causes a crash — a denial-of-service condition for any Node.js application using this native module.

The Fix

The fix is a single-character-level change with enormous security impact — replacing sprintf with snprintf and passing sizeof(line) as the size bound:

Before (vulnerable):

int len = sprintf(line, "%d,%s,%d\n", i, NAMES[c[i]], (int)(v[i] + it));

After (fixed):

int len = snprintf(line, sizeof(line), "%d,%s,%d\n", i, NAMES[c[i]], (int)(v[i] + it));

Why this works:

  1. snprintf() guarantees bounded writes: It will write at most sizeof(line) - 1 characters (63 bytes) plus a null terminator, regardless of how long the formatted output would be.

  2. Truncation over corruption: If the formatted string exceeds 63 characters, snprintf() truncates the output rather than overflowing the buffer. The return value len still reports what the full length would have been, but the buffer is never overwritten past its boundary.

  3. No behavior change for normal inputs: For benchmark strings that fit within 64 bytes (the expected case), the output is identical. The fix only activates as a safety net when inputs would have caused an overflow.

  4. sizeof(line) is compile-time constant: Since line is a stack array, sizeof(line) evaluates to 64 at compile time — zero runtime overhead for the bounds check.

Prevention & Best Practices

Immediate Rules

  • Never use sprintf() with fixed-size buffers. Always use snprintf() with an explicit size parameter.
  • Treat all format string targets as potentially too small. Even if you "know" the data fits today, maintenance changes or upstream data changes can break that assumption.
  • Check snprintf() return values. If len >= sizeof(line), truncation occurred — log it or handle the error.

Detection Tools

  • Semgrep: The rule utils.custom.buffer-overflow-strcpy caught this exact pattern. Configure it in CI to block PRs that introduce sprintf(), strcpy(), or strcat() on fixed buffers.
  • GCC/Clang warnings: Compile with -Wformat-overflow=2 to get compile-time warnings about potential sprintf() overflows.
  • AddressSanitizer (ASan): Build with -fsanitize=address during testing to catch overflows at runtime.

Coding Standards

  • Follow CERT C Coding Standard STR31-C: "Guarantee that storage for strings has sufficient space for character data and the null terminator."
  • Use snprintf() as the default for all string formatting in C. There is no valid reason to use sprintf() in modern code.

Key Takeaways

  • sprintf() into char line[64] is a ticking time bomb — the format "%d,%s,%d\n" with an unbounded NAMES[] string can trivially exceed 64 bytes.
  • snprintf(line, sizeof(line), ...) is a zero-cost fix that prevents stack corruption while preserving correct behavior for normal-length inputs.
  • Benchmark code is still production codebench/strbuild/strbuild.c ships with the package and runs in the same process as application code.
  • The len variable used in the subsequent loop (for (int j = 0; j < len; j++)) now safely reflects the truncated length, preventing out-of-bounds reads as well.
  • Static analysis in CI catches these patterns before they reach production — this was detected by Semgrep's utils.custom.buffer-overflow-strcpy rule automatically.

How Orbis AppSec Detected This

  • Source: Data entering through the NAMES[c[i]] lookup and integer values v[i] + it, which are derived from function parameters c and v passed into run_kernel().
  • Sink: sprintf(line, "%d,%s,%d\n", i, NAMES[c[i]], (int)(v[i] + it)) at bench/strbuild/strbuild.c:42 — an unbounded write into a 64-byte stack buffer.
  • Missing control: No size limit on the sprintf() write operation; the destination buffer size is never passed to the formatting function.
  • CWE: CWE-120 (Buffer Copy without Checking Size of Input)
  • Fix: Replaced sprintf(line, ...) with snprintf(line, sizeof(line), ...) to enforce a 64-byte maximum write.

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 common and dangerous vulnerabilities in C code — even in 2024. This specific instance in bench/strbuild/strbuild.c demonstrates how a seemingly harmless benchmark utility can harbor a high-severity stack corruption bug. The fix — a one-line change from sprintf to snprintf with sizeof(line) — is trivial to implement, has zero performance cost, and completely eliminates the overflow risk. If you write C code, make snprintf() your default. There is no excuse for unbounded string formatting in production codebases.

References

Frequently Asked Questions

What is a buffer overflow via sprintf()?

It occurs when sprintf() writes more data into a destination buffer than it can hold, because sprintf() has no parameter to limit the number of bytes written, potentially overwriting adjacent memory on the stack or heap.

How do you prevent sprintf() buffer overflows in C?

Use snprintf() instead, which accepts a maximum size parameter (e.g., `snprintf(buf, sizeof(buf), fmt, ...)`) and guarantees the output will not exceed the specified length, including the null terminator.

What CWE is buffer overflow via sprintf()?

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

Is using a large buffer enough to prevent sprintf() buffer overflows?

No. Regardless of buffer size, if input data is unbounded or attacker-controlled, it can always potentially exceed the buffer. Only size-bounded functions like snprintf() provide a guaranteed safety limit.

Can static analysis detect sprintf() buffer overflows?

Yes. Tools like Semgrep, Coverity, and GCC's -Wformat-overflow can flag sprintf() calls into fixed-size buffers as potential overflows, recommending snprintf() or equivalent bounded alternatives.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #108

Related Articles

medium

How Insufficient Password Hashing Cost Factor Happens in Node.js and How to Fix It

A bcrypt password hashing implementation in the User.js model was using a cost factor of 10, which falls below OWASP's 2024 recommendation of 12 for applications handling sensitive data. This fix upgrades the salt rounds from 10 to 12, increasing the computational work required to crack passwords by approximately 4x, significantly improving protection against brute-force attacks on this e-commerce platform.

high

How Arbitrary Code Execution via Template Imports Happens in JavaScript (lodash) and How to Fix It

A high-severity arbitrary code execution vulnerability (CVE-2026-4800) was discovered in lodash's template function, specifically in how it handles the `imports` option with untrusted input. The fix upgrades lodash from version 4.17.21 to 4.18.0 in the project's `package.json` and `yarn.lock`, eliminating the attack surface where crafted template imports could execute arbitrary code on the server.

critical

How Server-Side Request Forgery (SSRF) happens in Node.js IP address parsing and how to fix it

A critical SSRF vulnerability (CVE-2026-69192) was discovered in the ip-address npm package version 10.2.0, which could allow attackers to bypass IP address validation and access internal services. The fix upgrades the dependency to version 10.3.1, which properly handles edge cases in IP address parsing that previously allowed trust-boundary bypasses.

critical

How Sensitive Data Exposure happens in Python web applications and how to fix it

A critical sensitive data exposure vulnerability was discovered in `nodes/google_gemini.py` where the Google Gemini API key was returned in plaintext through a web endpoint. The fix masks the token in API responses, preventing credential theft from any client that queries the token endpoint. This protects downstream users of this Node.js library from unauthorized access to their Google Gemini services.

high

How Authentication Bypass happens in Next.js App Router with Turbopack and how to fix it

A critical authentication bypass vulnerability (CVE-2026-64642) was discovered in Next.js versions prior to 16.2.11, specifically affecting App Router applications using Turbopack with a single locale configuration. This vulnerability allowed attackers to bypass middleware and proxy protections, potentially gaining unauthorized access to protected routes and resources that should have been secured by authentication checks.

critical

How SQL Injection Happens in CSV-to-SQL Converters and How to Fix It

A critical SQL injection vulnerability was discovered in the `csv2sql()` function in `src/data/converter/csv.js`, where CSV data and table names were directly interpolated into SQL INSERT statements without sanitization. The fix implements input validation through identifier sanitization and proper value escaping, eliminating the attack surface while preserving legitimate functionality.