Skip to content
pwnsy
exploitationintermediate#format-string#memory-safety#exploitation#secure-coding#cwe

Format String Vulnerabilities: When Input Becomes Format

How uncontrolled format strings turn user input into read and write primitives, why the bug is so severe, and how to defend against it.

Formatting functions turn values into text. You hand them a format string full of placeholders and a list of values, and they weave the values into the placeholders. The security problem appears when the format string itself comes from untrusted input. The formatting function trusts the format completely, so whoever controls it controls what the function reads and, in some cases, writes. MITRE tracks this as CWE-134: Use of Externally-Controlled Format String.

This bug is narrower than a buffer overflow and often easier to fix, yet its impact can be just as severe, because it hands an attacker both a way to read memory and a way to write it. This guide explains the mechanism and the one-line discipline that removes it.

How formatting functions consume arguments

A formatting function reads its format string left to right, copying ordinary characters straight to the output. When it meets a format specifier, a placeholder that begins with a percent sign, it stops copying and instead pulls the next value from its argument list and formats that value according to the specifier. A specifier for an integer pulls an integer, a specifier for a string pulls a pointer to a string, and so on.

The function does not independently know how many arguments the caller actually passed. It trusts the format string to tell it. Each specifier in the format means take another argument. If the format contains more specifiers than the caller supplied arguments, the function keeps pulling from wherever the next arguments would live in memory, reading values that were never meant to be arguments at all.

The bug: input becomes the format

The vulnerability is a single mistake: passing user-controlled data as the format string. The correct pattern passes a fixed, trusted format and the user data as a separate value. The dangerous pattern passes the user data directly as the format.

When the attacker controls the format, they control the specifiers, and the specifiers are instructions to the formatting function. Two capabilities follow.

The read primitive

By supplying format specifiers, the attacker makes the function pull and print values from the argument area of memory, which on many platforms overlaps the stack. Because the caller passed no real arguments to match those specifiers, the function reads whatever sits in that region: saved values, pointers, and other stack contents. Printing them back to the attacker leaks memory.

This matters beyond curiosity. Leaked pointers reveal where code and data are loaded, which defeats address space layout randomization. A read primitive that discloses an address is often the first stage that makes a second memory bug reliably exploitable, a connection covered in exploit mitigations explained.

The write primitive

Some format specifiers do not print a value, they write. A specifier exists that stores the number of characters output so far into a memory location given by the corresponding argument. By controlling the format and the values in the argument area, an attacker can direct that write to an address of their choosing and control the value written through careful padding. This is a classic write-what-where condition, CWE-123, the same primitive many overflow exploits work hard to obtain. Here it falls out of a single misused format string.

One misuse, two primitives

A format string bug is unusually valuable to an attacker because it delivers both halves of an exploit at once. The read side leaks the addresses needed to bypass ASLR, and the write side provides the arbitrary write to hijack execution. Many other bug classes give one primitive and require chaining to get the other. Treat any uncontrolled format string as a full memory-corruption vulnerability, not a logging nuisance.

Read primitive versus write primitive

AspectRead primitiveWrite primitive
EffectDiscloses memory contentsModifies a chosen memory location
Typical useLeak pointers, defeat ASLR, steal secretsOverwrite a function pointer or control data
Attacker inputSpecifiers that print valuesA specifier that stores the output count
SeverityHigh, enables later exploitationCritical, direct path to control-flow hijack

Why the bug survives

Format string bugs persist for the same reasons across codebases: a value that is usually a fixed string occasionally gets replaced with a variable that carries user input, often in error and logging paths that are lightly tested. Logging is a frequent culprit, because a developer logs a message that already contains user data and accidentally makes that combined string the format. The code looks harmless and works fine until the input contains a specifier.

The saving grace is that the fix is mechanical and the detection is cheap, which is why this class should be extinct in well-tooled projects and only survives where warnings are ignored.

How to defend against format string bugs

Unlike overflows, this class has a clean and complete source-level fix.

  1. Always use a constant format string. Pass the fixed format the code author wrote, and supply user data as a data argument. If the data is the thing you want to print, use a format that prints a string and pass the data as that string argument. Never let input reach the format position.
  2. Enable and enforce compiler warnings. Compilers can warn when a format string is not a literal constant. Turn those warnings on, and treat them as errors so the pattern cannot merge.
  3. Run static analysis. Analyzers flag externally controlled format arguments reliably. Add the check to continuous integration so new instances are caught automatically.
  4. Fix logging paths specifically. Audit every logging and error-message call where user data appears. Ensure the user data is an argument, and the format is a constant.
  5. Prefer safer interfaces. Use formatting APIs and language features that separate the template from the data by design, so there is no format position for input to reach.
  6. Keep the mitigation stack on. ASLR, non-executable memory, and stack protection still apply and raise the cost of turning the write primitive into full control, as covered in secure coding practices.
This bug should never ship

Format string vulnerabilities are among the most preventable memory-safety bugs. A single rule, format strings are always constants written by the developer, eliminates the class, and compilers plus static analysis enforce the rule for free. If one reaches production, the gap is process, so wire the warning into the build and fail on it.

A closer look at the read primitive

To see why a read primitive falls out so cleanly, follow what happens on a platform where a formatting function reads its variadic arguments from the stack. The function has a fixed idea of where the first argument sits relative to the format string. Each time it meets a specifier, it advances an internal pointer to the next argument slot and reads whatever is there. The function never checks whether the caller actually placed a value in that slot. It simply reads the memory at the expected position.

When the attacker supplies the format and the caller supplied no matching arguments, the slots the function reads are whatever the stack held at those positions: saved register values, local variables of the calling frame, return addresses, and pointers into loaded modules. A specifier that formats an integer prints the raw contents of a slot. A specifier that formats a string treats the slot's contents as a pointer and prints the bytes it points to until it reaches a terminator. That second behaviour is powerful, because it lets an attacker dereference values found on the stack and read arbitrary memory that those values happen to address.

A position selector makes this systematic. Many formatting implementations support a syntax that names which argument to format directly, rather than consuming them in order. With it, an attacker can walk the argument area slot by slot, dumping the stack in a controlled sweep rather than crafting a specifier for each position. The output is a window into process memory, and among the values it reveals are addresses that betray where code and data are loaded. Those leaked addresses are exactly what an attacker needs to defeat address space layout randomization and to compute the target for a later write.

Why a leaked address is the prize

Modern exploitation is often two problems: find out where things are, then corrupt something now that you know the layout. A format string read solves the first problem directly. One leaked pointer into a known module lets an attacker calculate the base address of that module and, from there, the location of every function and gadget inside it. That is why a read-only format string bug is rarely just an information leak; it is frequently the enabling step for a full compromise.

A closer look at the write primitive

The write side rests on a single unusual specifier: the one that stores the count of characters written so far into a location supplied as an argument. On the surface it exists for a benign purpose, letting a caller learn how many bytes a format produced. In the hands of an attacker who controls the format and can influence the argument area, it becomes a write-what-where primitive, MITRE's CWE-123.

The where comes from a pointer sitting in the argument area, which the attacker arranges to be an address of their choosing, often by placing that address inside the format string itself so it lands in the stack region the specifiers walk. The what comes from the output count, which the attacker controls by padding the format to emit a chosen number of characters before the write specifier fires. Field width in a specifier lets the function emit a large, precise number of padding characters cheaply, so the attacker can drive the count to almost any value without producing a format string that is impractically long. To write a wide value without printing billions of characters, the classic method splits the write into smaller pieces at increasing addresses, writing a couple of bytes at a time and letting each write build on the running count.

With an arbitrary write, the attacker looks for a target whose corruption yields control of execution. On some platforms and older toolchains that meant overwriting an entry in a table of destructor or exit handlers. Elsewhere it means overwriting a saved return address, a function pointer stored in writable memory, or an entry in a procedure linkage table so a later call is redirected. The details vary by platform, but the shape is constant: turn the arbitrary write into a hijack of the next transfer of control.

A conceptual walkthrough

Putting the two primitives together, a typical exploitation of an uncontrolled format string proceeds in stages, without any specific tool or target named here.

First, confirm the bug. The attacker sends input containing several read specifiers and observes whether the program's output reflects stack contents rather than printing the input literally. Output that shows unexpected numbers or garbled memory instead of the specifiers themselves confirms that input is reaching the format position.

Second, locate the input on the stack. Using the position selector, the attacker sweeps argument slots until the output reveals a recognisable marker they placed in their own input. Knowing which slot holds attacker-controlled bytes is essential, because the write primitive needs its target address to sit in a slot the format can reference.

Third, leak addresses. The attacker reads slots that hold pointers into loaded modules and stack frames, then computes module base addresses from the known offsets of the leaked symbols. This defeats layout randomization and yields concrete addresses for the next stage.

Fourth, perform the write. The attacker crafts a format that embeds the target address in the controllable slot, pads the output to produce the desired value, and fires the count-storing specifier to write it. Repeating this a few times writes a full pointer-sized value.

Fifth, gain control. The chosen write redirects execution at the next opportunity, whether that is a function pointer being called or a return unwinding to an address the attacker now controls.

Each stage depends on the last, which is why a format string bug is best understood as a chain rather than a single trick. The defensive lesson is that breaking the very first link, input reaching the format slot, dissolves the entire chain.

Detection signals

Format string bugs are cheap to find precisely because the dangerous pattern is syntactically distinct. Several signals catch it at different stages.

At build time, compilers can warn when a format argument is not a compile-time constant. This is the strongest and earliest signal, and treating the warning as an error prevents the pattern from ever merging. Static analysis tools flag calls where a formatting function receives externally influenced data in the format position, and they trace data flow from input sources to the format argument, so they catch cases the compiler misses because the value is a variable rather than a direct parameter.

In code review, the signal is any formatting call whose first argument is a variable rather than a literal, especially in logging and error-reporting paths where a message that already contains user data is passed as the format. A grep across a codebase for formatting calls followed by a non-literal first argument surfaces candidates quickly.

At runtime, a program that prints stack garbage, leaks pointer-like values, or crashes when fed input containing percent signs is exhibiting the classic behaviour. Fuzzing with inputs rich in format specifiers reliably triggers the read side, and monitoring for crashes or anomalous output under such inputs is an effective dynamic detector.

StageSignalTool or method
BuildNon-constant format argument warningCompiler format-security warnings
StaticTainted data reaching a format slotStatic analysis, data-flow tracing
ReviewFormatting call with a variable first argumentCode review, targeted grep
RuntimeStack leakage or crashes on percent-rich inputFuzzing, crash monitoring

Common mistakes

The recurring way this bug slips in is the logging shortcut. A developer has a message that already contains user data and passes that whole message as the format string, because it prints correctly during testing where the data never contains a specifier. The code looks clean and behaves until the day an input carries a percent sign. The fix is to always pass a constant format and hand the message as a data argument.

A second mistake is trusting input that has passed through several layers. Data that arrives sanitised for one purpose, say stripped of shell metacharacters, may still carry format specifiers, because percent signs are harmless in most other contexts. Sanitisation aimed at a different threat does nothing for this one.

A third mistake is assuming the bug is only an information leak and deprioritising it. Because the same misuse yields both a read and a write, an uncontrolled format string should be treated as a full memory-corruption vulnerability, not a cosmetic logging flaw. Ranking it as low severity because the immediate symptom is odd output underestimates it badly.

A fourth mistake is relying on runtime mitigations to make the bug safe. Layout randomization, non-executable memory, and stack protection raise the cost of exploitation, and they are worth keeping on, but they do not remove the vulnerability. The read primitive is designed to defeat randomization, and the write primitive targets data the mitigations do not all cover. The only complete fix is at the source, keeping input out of the format position.

Frequently asked questions

What exactly makes a format string vulnerable? The vulnerability is that user-controlled data reaches the format argument of a formatting function. The function treats the format as a trusted instruction stream, so whoever controls it controls what the function reads and, through the count-storing specifier, what it writes.

Is this the same as a buffer overflow? No, but the impact overlaps. A buffer overflow corrupts memory by writing past a buffer's bounds. A format string bug reads and writes memory through misused specifiers. Both can reach a write-what-where condition, so both can end in control-flow hijack, which is why the severity is comparable.

Why can it read memory without any overflow? Because the formatting function reads arguments from a fixed set of positions and trusts the format to tell it how many there are. Extra specifiers make it read positions the caller never filled, which on many platforms means reading whatever is on the stack.

Does address space layout randomization stop it? No. The read primitive exists to leak the very addresses that randomization tries to hide. Leaked pointers let an attacker compute module bases and defeat the randomization, so it raises the cost slightly but does not close the hole.

How do I fix it correctly? Always call formatting functions with a constant format written by the developer, and pass user data as a separate data argument. If you want to print a user-supplied string, use a format that prints a string and hand the string as the argument, never as the format.

Can static analysis catch every case? The common cases, yes, and cheaply. Compiler format-security warnings and static analysers reliably flag non-constant format arguments and tainted data reaching the format slot. Wiring these into the build and failing on them keeps the class out of production.

Does this only affect C and C-family languages? The classic read and write primitives are most direct in C-family formatting functions that read variadic arguments from the stack, so that is where the bug is most dangerous. Higher-level languages with their own formatting facilities can still suffer information disclosure or denial of service when untrusted input reaches a format template, even where memory corruption is not possible. The safe discipline is the same everywhere: keep untrusted input out of the format position and pass it as data.

Why is logging the usual place this bug appears? Logging code frequently assembles a message that already contains user data, and it is tempting to pass that finished message straight to a formatting log call. During testing the data rarely contains a percent sign, so the code passes review and ships. The correct form logs a constant format and supplies the user data as an argument, which costs nothing and removes the risk entirely.

If the write specifier is disabled, is the bug harmless? Some hardened runtimes restrict or disable the count-storing specifier, which removes the direct write primitive. The read primitive still works, so the bug remains a serious information-disclosure flaw that can leak secrets and defeat layout randomization. Treat a disabled write specifier as a mitigation that lowers the ceiling, not as a fix, and still keep input out of the format position.

The format string vulnerability is a lesson in how much power sits inside routine functions. A formatting call trusts its format completely, so the moment untrusted input reaches that slot, ordinary output code becomes a read and write engine into process memory. The defense is not a mitigation to layer or a filter to tune, it is a rule to hold: the format is always yours, the data is always theirs. For which memory-safety bug classes attackers actually exploit, see our Exploit Intelligence dashboard.

Sources & further reading

Sharetwitterlinkedin

Related guides