Format string injection happens when user input becomes the template a formatting function interprets, so the attacker's characters are read as instructions instead of text. MITRE CAPEC catalogues it as CAPEC-135 Format String Injection, a Standard-level pattern rated High for likelihood and High for severity, with consequences that run from reading data to modifying memory and running arbitrary code. Its weakness is the MITRE CWE entry Use of Externally-Controlled Format String, which describes it as a classic C and C++ issue that is now rarer because it is easy to discover. Easy to discover is not the same as gone: it survives in logging paths, error handlers, firmware and internationalisation code, and it has cousins in memory-safe languages. This guide is for developers, code reviewers and application security engineers. It explains the mechanism at a defensive level, how it differs across languages, how to find it with compilers and static analysis, and how to remove it for good. For the catalogue itself, see the MITRE CAPEC Guide.
Key takeaways
- The rule is one line. SEI CERT rule FIO30-C says never call a formatted I/O function with a format string containing a tainted value; pass user data as an argument to a constant format.
- In C and C++ it is a memory corruption bug. CWE explains that formatting functions cannot tell how many arguments were passed, so directives read the stack, and the %n directive writes to memory.
- Compilers already find most cases. GCC's -Wformat-security flags non-literal format strings with no arguments, but it is not enabled by -Wall; turn it into an error with -Werror=format-security.
- Memory-safe languages still leak. Python's str.format resolves attribute and index lookups in field names, and logging templates built from user input change which values are logged.
- Error and logging paths are where it hides. CWE notes black box testing has limited effect because format strings often sit in rarely exercised error handling.
How the attack pattern works
CAPEC-135 describes an adversary who includes formatting characters in a string input field, relying on the application to pass that string to a function that interprets formatting characters. Its single prerequisite is an application that accepts string input, fails to neutralise formatting characters, and processes the string with such a function. CAPEC's execution flow has three phases:
- Explore: survey the application's entry points, including parameters, external variables and configuration values.
- Experiment: probe each user-controllable input with formatting characters to see whether output changes.
- Exploit: work out how the input is used and turn the behaviour into a memory read or write.
The CWE entry explains why this is dangerous in C. Variadic formatting functions take as many arguments as the format string asks for, whether or not they were passed, so each extra directive reads the next value from the stack or registers. The %s directive treats one of those values as a pointer and reads a string from it, and the %n directive writes the number of characters printed so far to an address, which CWE describes as a path to a write-what-where condition and arbitrary code execution. Width specifiers can also produce very large output and overflow buffers. CAPEC rates the skill needed as High: finding the bug is easy, turning it into a reliable exploit is not.
CAPEC-135 is a child of Parameter Injection and the parent of CAPEC-67 String Format Overflow in syslog(), the specific case of passing user data as the format argument to the Unix logging call. CWE's observed examples include several incorrect syslog calls.
How it behaves in different languages
| Language or context | Vulnerable shape | Impact | Safe pattern |
|---|---|---|---|
| C and C++ printf family | printf(user), fprintf(stderr, msg) where msg contains user data | Stack and memory disclosure, memory writes, crashes, code execution | printf("%s", user) or fputs(msg, stderr) |
| POSIX syslog | syslog(LOG_INFO, msg) with user data in msg | As above, in a privileged daemon (CAPEC-67) | syslog(LOG_INFO, "%s", msg) |
| Windows C runtime | printf family with user format | %n is disabled by default and invokes the invalid parameter handler; reads and crashes remain | Constant format; never call _set_printf_count_output(1) |
| Python str.format | user_template.format(obj) | Field names such as {0.attr} or {0[key]} call getattr and indexing, exposing object internals or secrets | Fixed templates; string.Template for translator-supplied text |
| Go fmt | fmt.Printf(message) | Garbled output and misleading logs, not memory corruption | fmt.Print(message) or a constant format |
| Parameterised logging (Java SLF4J style) | Concatenating user data into the message template | Placeholders in the data change which values fill the template | Constant template, user data as arguments |
The OWASP Logging Cheat Sheet gives the Java example directly: concatenating a username into a log template means "placeholders in username can change which values fill the template", and the fix is a hard-coded template with the username as an argument. The CWE entry also lists Perl and Python among affected languages.
Where it shows up in real systems
- Logging and error helpers: wrappers such as log_error(const char *msg) that pass msg straight to a printf-style function. The noncompliant example in FIO30-C is exactly this, an incorrect-password message built with the user name and then used as the format.
- Network daemons and protocol handlers: CWE's observed examples include format strings in NNTP server responses and incorrect syslog calls.
- Filenames and metadata echoed in errors: CWE lists a case where errors and warnings triggered format specifiers embedded in a .bmp filename.
- Internationalisation catalogues: translated message files are format strings by design. CAPEC's example is a chained issue (CVE-2007-2027) where an untrusted search path loaded a malicious message catalogue. CWE notes that an externally supplied format from a trusted, admin-only file may be acceptable, so the real control is who can write the catalogue.
- Firmware and embedded code: long-lived C code bases with custom printf implementations and debug consoles. See Embedded and Firmware Secure Coding.
Finding it: compilers, analysers and review
| Method | What it catches | Limits |
|---|---|---|
| GCC -Wformat-security | printf and scanf calls whose format is not a literal and that have no format arguments, such as printf(foo) | Needs -Wformat; not enabled by -Wall |
| GCC -Wformat-nonliteral | Any non-literal format string the compiler cannot check | Noisy on code that legitimately builds formats; not applied to va_list functions |
| GCC -Wformat=2 | Currently -Wformat, -Wformat-nonliteral, -Wformat-security and -Wformat-y2k together | Same as its parts |
| format attribute on wrappers | Lets the compiler apply the same checks to your own logging functions | Must be added to each wrapper |
| Go vet printf analyser | Printf-like calls with a non-constant format and no other arguments | Only recognised Printf-like functions |
| SAST with data flow | Tainted data reaching a format argument across functions; CERT lists rule mappings in several commercial and open source analysers | Quality varies with taint models; see the SAST Triage Guide |
| Manual review | Wrappers, i18n loading, error paths | Effort; CWE calls it highly cost-effective |
| Fuzzing | Crashes from formatting characters in inputs | Partial coverage of rare error paths; see the Fuzzing Guide |
CWE's detection notes are worth taking at face value: automated static analysis with data flow often finds this weakness, while black box testing has limited effectiveness because the vulnerable call is often in an error-handling path that a scanner never triggers.
How to fix and harden
- Fix the call, not the input. FIO30-C's compliant solutions are either a non-formatting function (fputs instead of fprintf) or a constant format with the user value as an argument. Filtering percent signs from input is a weak substitute: CAPEC's mitigations list input validation for formatting characters, but CWE's primary implementation mitigation is static format strings.
- Turn warnings into build failures. Add -Wformat -Wformat-security and -Werror=format-security to every C and C++ build, then consider -Wformat-nonliteral for security-sensitive components. Run go vet in CI for Go services.
- Annotate wrappers. Mark logging and error functions with the compiler's format attribute so their callers are checked, and make the wrappers take a format plus arguments rather than a preformatted message.
- Keep %n off. On Windows, leave %n disabled (the default) and treat any call to _set_printf_count_output(1) as a review finding. Where possible, CWE advises using functions that do not support %n at all.
- Control message catalogues. Load translations only from paths writable by administrators, verify catalogue integrity in the build, and check that translated strings keep the same specifiers as the source.
- Use templates designed for untrusted text in memory-safe languages. In Python, do not call .format on user-supplied templates; the standard library documents string.Template as the simpler substitution for internationalisation. In Java and similar, keep logging templates constant.
- Move new code to memory-safe languages. CWE's requirements-phase mitigation is a language not subject to the flaw. See the Memory-Safe Languages Guide.
Rolling out in an existing code base
- Build once with -Wformat=2 in a side job and record the warnings without failing the build.
- Triage: fix every -Wformat-security finding first, since each is a likely FIO30-C violation.
- Enable -Werror=format-security for the main build once the count reaches zero, so regressions fail fast.
- Add SAST taint rules for format arguments and review wrapper functions by hand.
- If a third-party library breaks the build, scope the flag per target instead of disabling it globally, and track the exception.
Detection and measurement
Exploitation attempts often show up as formatting characters in places they never belong: usernames, filenames, HTTP headers and protocol fields containing percent sequences, and crashes in logging or error code. Useful signals:
- WAF and application logs with repeated format directives in identity or filename fields.
- Crash reports whose top frames are in vfprintf-style functions or logging wrappers.
- On Windows, invalid parameter handler events from the C runtime in production services.
Metrics that show the programme is working:
- Share of C and C++ build targets compiling with -Werror=format-security.
- Open -Wformat-security and -Wformat-nonliteral findings, trending to zero.
- Logging wrappers carrying a format attribute, as a share of all wrappers.
- Mean time to fix SAST format string findings.
- Fuzz harness coverage of error and logging paths.
Framework mapping
| Framework | Reference | How it applies |
|---|---|---|
| MITRE CAPEC | CAPEC-135, child of Parameter Injection; parent of CAPEC-67 | Pattern definition, prerequisites and mitigations |
| MITRE CWE | Use of Externally-Controlled Format String | Weakness definition, mitigations and detection methods |
| SEI CERT C | FIO30-C Exclude user input from format strings | Severity High, likelihood Likely, priority P18, level L1 |
| NIST SP 800-53 Rev 5 | SI-10 Information Input Validation, SA-11 Developer Testing and Evaluation, SA-15 Development Process, Standards, and Tools | Input handling, static analysis and compiler settings in the build |
| NIST CSF 2.0 | PR.PS-06 | Secure software development practices |
Common failure patterns
- Relying on -Wall: GCC does not include -Wformat-security in -Wall, so builds that "use all warnings" still miss it.
- Preformatted messages passed to wrappers: the wrapper calls printf(msg) and every caller inherits the bug.
- Sanitising input instead of fixing the call: filters miss encodings and other entry points, while the constant-format fix is complete.
- Writable translation directories: a message catalogue path that attackers can influence turns i18n into injection.
- Assuming memory-safe means immune: user templates passed to Python's str.format or concatenated into log templates still disclose data.
- Testing only the happy path: the vulnerable call sits in an error handler the test suite never reaches.
Practitioner checklist
- Compile C and C++ with -Wformat -Wformat-security -Werror=format-security.
- Run go vet in CI and fail on printf analyser findings.
- Add format attributes to every logging and error wrapper.
- Replace printf(user) style calls with a constant format or fputs.
- Keep %n disabled and flag any code that enables it.
- Restrict message catalogue paths to administrator-writable locations.
- Ban user-supplied templates for str.format and similar APIs.
- Add SAST taint rules and fuzz harnesses that reach error paths.
Standards and references
- MITRE CAPEC: CAPEC-135 Format String Injection
- MITRE CWE: Use of Externally-Controlled Format String
- SEI CERT C Coding Standard: FIO30-C Exclude user input from format strings
- GCC: Options to Request or Suppress Warnings
- Microsoft Learn: _set_printf_count_output
- Go: printf analyser
- Python: string, common string operations
- OWASP: Logging Cheat Sheet
Related NHI Mgmt Group resources: MITRE CAPEC Guide · C and C++ Secure Coding Guide · Input Validation and Output Encoding Guide · Memory-Safe Languages Guide