Description
printf-style functions interpret percent directives in their format argument and read the corresponding values from subsequent arguments. Passing untrusted input as the format lets an attacker inject directives such as %x, %s, or positional arguments. These may expose process memory or crash the process through invalid argument access. The %n directive writes the number of characters output so far to the memory pointed to by its argument, potentially allowing memory corruption.
Limiting output length with snprintf() does not make the format string safe. POSIX syslog() formats message text similarly to printf, so its message argument needs the same protection.
Potential impact
- Exposure of sensitive information, including process memory and addresses
- Process failure through invalid memory access or excessive output
- Memory corruption or code execution with the process's privileges, depending on the environment and subsequent arguments
Remediation
- Keep the format string as a code literal, and pass untrusted text as an ordinary value argument, such as the value for
%s. - Apply the same principle to buffer output functions,
va_listvariants, and system logs. - On a C++23-conforming implementation,
std::print("{}", input)can print text with a literal format. Where stream output is suitable,std::cout << inputalso avoids interpreting percent directives. - Do not assume a helper called
sanitize_format()or replacing%characters makes input safe. Use a fixed format and ensure the argument count and types match its directives. - Enable format diagnostics such as GCC's
-Wformat=2to catch mistakes. Compiler warnings do not replace fixed format strings.
Examples
Standard output
Before
#include <stdio.h>
int main(int argc, char **argv) {
if (argc != 2) {
return 2;
}
printf(argv[1]);
return 0;
}
Percent directives in argv[1] are interpreted as format syntax.
After
#include <stdio.h>
int main(int argc, char **argv) {
if (argc != 2) {
return 2;
}
printf("%s", argv[1]);
return 0;
}
The format is a literal, and argv[1] is treated only as a string value.
std::printf() in C++
#include <cstdio>
void show_user_input(const char *input) {
// Before: input is interpreted as a format string.
std::printf(input);
// After: input is a value argument for %s.
std::printf("%s", input);
}
The std:: namespace does not change C-style format string semantics. With C++23's std::print(), keep the format literal as well, as in std::print("{}", input).
System logs
#include <syslog.h>
void log_user_input(const char *input) {
/* Before: input is interpreted as a format string. */
syslog(LOG_NOTICE, input);
/* After: input is a value argument for %s. */
syslog(LOG_NOTICE, "%s", input);
}
References
- POSIX.1-2024
fprintf()functions - POSIX.1-2024
vfprintf()functions - POSIX.1-2024
syslog() - ISO/IEC 14882:2024 Programming languages — C++
- Microsoft C++ Standard Library
<cstdio> - GCC 16.1 libstdc++
<cstdio>implementation - GCC 16.1 libstdc++ C++23
<print>implementation - SEI CERT C FIO30-C: Exclude user input from format strings
- GCC format string warning options
- CWE-134: Use of Externally-Controlled Format String