Safeguard
Vulnerability Analysis

Format string vulnerabilities explained

Format string bugs let attackers turn a printf call into memory disclosure or arbitrary writes. Here's how CWE-134 works, real CVEs, and fixes.

James
Principal Security Architect
6 min read

Format string vulnerabilities happen when a program hands attacker-influenced data directly to a formatting function — like C's printf, fprintf, or syslog — as the format argument instead of as a value to be formatted. The difference is one line of code: printf(user_input) versus printf("%s", user_input). The first lets anyone who controls user_input embed conversion specifiers such as %x, %s, or %n and have the runtime interpret them, reading arbitrary stack memory, leaking pointers and credentials, crashing the process, or in the worst case writing to memory and hijacking execution. Tracked as CWE-134, the class was popularized by a wave of remote root exploits against Unix daemons around 2000 and has never fully disappeared — it keeps resurfacing in embedded firmware, logging code, and C/C++ services that predate modern compiler hardening. This post breaks down how the bug works, which real CVEs prove the impact, and how to catch it before it ships.

What is a format string vulnerability?

A format string vulnerability is any case where externally controlled input reaches the format-specifier argument of a printf-family function, letting an attacker choose what the program reads from or writes to memory. In C, functions like printf, sprintf, fprintf, syslog, and err/warn all take a format string containing conversion specifiers (%d, %s, %x, %n) followed by a matching list of arguments. If the format string itself comes from user input — a filename, a username, an HTTP header, a log message — and the developer omitted the arguments the specifiers expect, the function pulls whatever happens to be next on the stack or in registers and treats it as those arguments. %x dumps raw stack words as hex, %s dereferences a stack value as a pointer and prints memory until it hits a null byte (often crashing on invalid pointers), and %n writes the number of bytes output so far to the address it's given — turning a formatting bug into an arbitrary-write primitive.

How does a format string exploit actually work?

A format string exploit works by feeding crafted specifiers into the vulnerable call to walk the stack, leak memory, or corrupt it. An attacker typically starts with a probe string like %x.%x.%x.%x.%x to see whether the output changes — confirming the format string is attacker-controlled — then uses repeated %x or %08x sequences to walk down the stack frame and locate useful values: saved return addresses, stack canaries, or a pointer to attacker-supplied data already sitting in the buffer. Once an attacker gets a pointer under their control onto the stack in the right position, %n (or its width-limited siblings %hn, %hhn) writes an attacker-chosen byte count to that address, enabling a four-byte-at-a-time overwrite of function pointers, GOT entries, or return addresses. This is slower and fiddlier than a classic stack buffer overflow, but it doesn't require any bounds-check failure at all — the vulnerable line of code is doing exactly what printf is documented to do.

What real CVEs show format string bugs causing actual damage?

Two CVEs a decade apart show the pattern moving from remote-root daemons to privilege-escalation tooling. CVE-2000-0573 hit wu-ftpd 2.6.0's site exec command: the FTP daemon passed a user-supplied filename straight into a vsnprintf call as the format argument, and because wu-ftpd ran as root, a crafted %n sequence gave remote attackers a root shell — one of the exploits that made format string bugs infamous in the security community in 2000. Twelve years later, CVE-2012-0809 hit sudo itself: versions before 1.8.3p2 passed command-line arguments into the internal sudo_debug() function's format string when debugging output was enabled, letting a local user crash sudo or potentially leak process memory. Fixed in February 2012, it's a reminder that even security-critical, heavily audited tools ship this bug when a debug or logging code path is added later without the same scrutiny as the main request path. The same class shows up regularly in embedded and IoT firmware CVEs today, where vendors still build against older libc and toolchains with hardening flags disabled.

Why do format string bugs still turn up in 2026 if compilers can catch them?

Format string bugs persist because the compiler warnings that catch them are opt-in, not universal, and a huge amount of C/C++ code — especially firmware, drivers, and vendored third-party libraries — is built without them enabled. GCC's -Wformat and -Wformat-security have existed since the late 1990s and can flag non-literal format strings passed to printf-family functions, and glibc has refused to honor %n in writable (non-read-only) format strings by default since glibc 2.4 in 2006 specifically to blunt this exploit class. But those protections only apply if the build actually passes -D_FORTIFY_SOURCE=2 -Wformat -Wformat-security, and cross-compiled embedded toolchains, legacy Makefiles, and vendored SDKs frequently don't. Language runtimes with type-safe formatting — Go's fmt.Sprintf, Rust's format! macro, Python's f-strings — have mostly designed the vulnerability class out by making the format string a compile-time literal, but plenty of Python and Java code still builds log messages by concatenating user input into a template string passed to a logging call, which reintroduces a related log-injection risk even without the classic %n memory-corruption angle.

How do you find and fix a format string vulnerability?

You find format string vulnerabilities by tracing every printf-family call back to confirm its first argument is a fixed string literal, not a variable — and you fix them by making the format string a literal and passing user input as an argument instead. The one-line fix for printf(user_input) is printf("%s", user_input); the same pattern applies to syslog(msg)syslog(LOG_INFO, "%s", msg) and fprintf(fp, filename)fprintf(fp, "%s", filename). Static analyzers (Clang's -Wformat-nonliteral, Cppcheck, Coverity) flag non-literal format arguments automatically, and it's a rule most SAST tools have enforced for over a decade — the gap is almost always in build pipelines that don't run those checks, or in third-party/vendored code that ships pre-built. At runtime, enabling _FORTIFY_SOURCE=2 and compiling with stack canaries limits the blast radius even if a bug slips through, but it's not a substitute for the source-level fix, since information-disclosure via %x/%s still works even when %n writes are blocked.

How Safeguard Helps

Safeguard's reachability analysis flags whether a format-string CVE or SAST finding — like a non-literal printf call inherited from a vendored library — actually sits on a path your code invokes, so you're not stuck triaging every CWE-134 hit in a dependency tree that never gets called at runtime. Griffin AI reads the surrounding call site to confirm whether the format argument genuinely originates from untrusted input (an HTTP header, a filename, a CLI argument) before it's surfaced, cutting down the false positives that make format-string findings easy to ignore. Safeguard generates and ingests SBOMs across your C/C++, Go, and Python services so a vulnerable version of a library like wu-ftpd-era FTP code or an outdated sudo build is visible the moment it enters your environment, not months later during an audit. When a fix is available, Safeguard opens an auto-fix PR that swaps the unsafe call for its literal-format equivalent, so the one-line correction ships without a manual grep-and-patch exercise across every affected repo.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.