Externally Controlled Format Strings in C/C++

Risks of using untrusted input as a printf-style format string

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_list variants, 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 << input also 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=2 to catch mistakes. Compiler warnings do not replace fixed format strings.

Examples

Standard output

Before

c
#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

c
#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++

cpp
#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

c
#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