CWE-134: Use of Externally-Controlled Format String
Overview
Use of Externally-Controlled Format String happens when untrusted input is passed as the format (template) argument to a formatting function such as printf, String.format, or %/.format() string interpolation, rather than as a value to be substituted into a template the application wrote. The attacker then controls how many arguments the function expects and how it interprets them. In C, this is a direct route to reading or writing arbitrary memory (%x, %n); in memory-safe languages the same root cause typically produces a denial-of-service exception or, where the language's format syntax supports attribute/item traversal, an information-disclosure path into object internals.
Relationship to Other CWEs
Use of Externally-Controlled Format String is a Base-level weakness that is a specific case of CWE-668 (Exposure of Resource to Wrong Sphere) - the format string itself is the "resource" exposed to a sphere (the attacker) that shouldn't control it. In C/C++, a format string bug that reaches %n can escalate into an arbitrary memory write, overlapping with CWE-787 (Out-of-bounds Write). That difference in severity between languages matters when scoping a finding.
OWASP Classification
A05:2025 - Injection
Risk
Critical in C/C++, Medium elsewhere: In C/C++, format string attacks can read arbitrary memory (leaking passwords, keys, or stack/heap addresses), write arbitrary memory via %n (a direct path to code execution), or crash the process. In managed languages (Java, Python, PHP), the same root cause typically produces an unhandled exception (denial of service) or, in languages whose format syntax allows attribute/item access, disclosure of object internals the application didn't intend to expose. Two consequences are easy to miss when scoping a managed-language finding, because neither depends on attribute or item traversal. Where the call supplies several arguments, positional specifiers (%3$s, {2}) let the attacker's template print an argument the application's own template never showed. And a single width specifier is a cheap denial of service in every one of them - %2000000000s asks for a two-billion-character result, which is memory exhaustion rather than a formatting exception, so the try/catch a team usually reaches for does not see it.
Remediation Steps
Core Principle: Never pass untrusted input as the format/template argument to a formatting function; the format must be a value the application wrote, with untrusted data only ever appearing as a substituted argument.
Trace the Data Path
- Source: Any string from user input, a file, a database, or a network request that could reach a formatting call as the template itself
- Sink: The format/template argument of a
printf-family function,String.format/equivalent, or a%/.format()string-interpolation call - Missing Controls: No check that the format argument is a fixed, application-authored literal before the untrusted string reaches that position
Use Literal Format Strings, Everywhere (Primary Defense)
- The format argument must be a compile-time or otherwise fixed literal in every call site, not just the one a scan finding names - search the whole file and its callers for other calls to the same formatting function
- Untrusted data always goes in the argument list to be substituted, never in the format position:
format("%s", user_input), notformat(user_input) - For logging specifically, use the logging framework's parameterized placeholder syntax instead of building or passing the message as a format template
If a Format Must Genuinely Be Selected Dynamically (Rare)
- Replace free-form user input with a fixed key into an application-defined allowlist of templates the application itself wrote
- Never construct the format string by concatenating or otherwise transforming user input
Harden as Defense in Depth
- Enable compiler/interpreter warnings that flag a non-literal format argument where the language/toolchain supports it
- Keep an unhandled formatting error from surfacing a stack trace or other internal detail to the client; that protection is independent of this fix
Test with Format String Payloads
- Specifiers that read more values than are supplied (
%x %x %x %x, or the language's equivalent) - should be treated as literal text, not resolved - Specifiers that don't match the argument type - should be rejected or treated as literal, not crash unhandled
- A specifier that would attempt a write, where the language supports one (
%nin C) - should never reach a call where the format is attacker-controlled - A specifier selecting an argument by position (
%3$s,{2}) where the call passes more than one - should be unreachable; where it is reachable it prints that argument whatever the intended template displayed - A single specifier with an absurd width (
%2000000000s) - should be unreachable. Assert on this one separately from the exception cases: it is a memory-exhaustion test, and a handler that catches formatting exceptions passes it while the process still dies - Confirm normal, legitimate formatted output still renders correctly
- Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
// VULNERABLE - pseudo-code
format(user_input) // user_input is used AS the template
// Attack: user_input contains format specifiers the caller never intended to supply arguments for
// Result: unexpected values are read from the call stack/heap (native code), or an unhandled
// exception/attribute-traversal occurs (managed languages)
Why this is vulnerable: a formatting function learns how many arguments to consume from the string, not from the call. The caller here passed none, so every specifier inside user_input directs the function to fetch an argument that was never supplied - it takes whatever the calling convention says the next argument would have been, which in native code is live stack or register contents. The attacker is not supplying data to be printed; they are supplying instructions about what the function should go and read.
The distance between the vulnerable call and the correct one is a single literal. format("%s", user_input) treats the input as a value and is safe whatever it contains; format(user_input) treats it as a template and is not. Nothing about the shorter form looks like it is missing an argument, which is why the pattern keeps reappearing in logging wrappers and error handlers, where the message already is the string to be printed.
Secure Patterns
// SECURE - pseudo-code
format("User: %s", user_input) // literal template; user_input is a substituted value only
Why this works: The template is fixed by the application, so an attacker cannot change how many values the call expects or what the call does with them. User-controlled data is only ever substituted into a placeholder the application chose, never interpreted as a formatting instruction.
Common Pitfalls
- Fixing the one call site a scan finding named, but not the others in the same file: Format-string bugs rarely occur in isolation - a nearby log call, error handler, or secondary output path built the same way remains exploitable after only the reported line is changed.
- Concatenating a literal prefix onto user input and using the result as the format: A prefix like
"User said: " + user_inputused as the template still lets any format specifier appearing later in the user-supplied portion be interpreted - a literal prefix doesn't neutralize specifiers elsewhere in the same string. - Denylisting specific dangerous specifiers instead of using a literal template: Stripping only the most severe specifier (
%nin C) while still passing attacker-controlled text as the format leaves the remaining specifiers (%x,%s, or the managed-language equivalents) available for information disclosure or a crash. - Assuming a memory-safe language means the finding can be closed as low-risk: The write-primitive risk (
%n) is C/C++-specific, but the same root cause still produces reliable denial-of-service exceptions or, in languages whose format syntax supports attribute/item access, information disclosure - it's a different consequence, not a non-issue.
Language-Specific Guidance
- C -
printf/sprintf/syslogfamily,%nwrite primitive,-Wformat-securityand what it does not cover - Java -
String.format/Formatter/MessageFormat, exception-based denial of service, and which flagged logging calls are real (the ones that pass parameters) - Python -
.format()attribute/item traversal,%operator, parameterized logging - PHP -
printf/sprintf/vsprintf,ArgumentCountErroron PHP 8+