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

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.