Format string injection

Externally controlled format strings

Description

Using untrusted input as the format string in String.format, System.out.printf, or Formatter.format lets an attacker insert format specifiers such as %s, %d, or positional references such as %1$.... These can cause argument-count or type errors, or expose other arguments supplied to the same call in an unintended way.

Potential impact

  • Service errors: MissingFormatArgumentException or IllegalFormatConversionException can interrupt request processing.
  • Information disclosure: positional specifiers such as %1$... and %2$... can expose other arguments supplied to the call.
  • Log corruption: %n can introduce new lines and disrupt log structure or monitoring.
  • Resource exhaustion: extremely large widths or precision, such as %1000000s, may consume excessive memory or CPU.

Remediation

  • Keep format strings constant and pass user input only as argument values.
  • To print input as text, use the fixed format string "%s".
  • If users must select a pattern, permit only a small allow-list of patterns and use a default for other values.
  • Use parameterized logging, such as SLF4J logger.info("User: {}", user). Handle newlines and other log control characters separately.

Examples

Before

java
import javax.servlet.http.HttpServletRequest;

public class BadFormatExample {
    public void printUser(HttpServletRequest req) {
        String pattern = req.getParameter("pattern"); // 외부 입력
        String user = req.getParameter("user");
        // BAD: 포맷 문자열에 외부 입력 사용
        System.out.printf(pattern, user);
    }
}

After

java
import javax.servlet.http.HttpServletRequest;
import java.util.Set;

public class GoodFormatExample {
    private static final Set<String> ALLOWED_PATTERNS = Set.of("User: %s", "Hello, %s!");

    public void printUser(HttpServletRequest req) {
        String pattern = req.getParameter("pattern");
        String user = req.getParameter("user");

        // 기본은 고정 포맷 사용
        String safePattern = "User: %s";
        // 포맷을 외부에서 받아야 한다면 허용 목록으로 제한
        if (pattern != null && ALLOWED_PATTERNS.contains(pattern)) {
            safePattern = pattern;
        }
        System.out.printf(safePattern, user);
    }

    public void echoRaw(HttpServletRequest req) {
        String text = req.getParameter("text");
        // 입력을 그대로 보여주려면 포맷 문자열은 항상 고정("%s")
        System.out.printf("%s", text);
    }
}

Explanation:

  • Before: External input becomes the format string. This call supplies only one argument, so %2$s causes a missing-argument exception, while %d does not match the string argument's type. Positional specifiers do not read arbitrary memory that was not passed to the call.
  • After: The format is fixed or selected from a limited allow-list. Input is passed as an argument, so format specifiers within it are not interpreted. Apply any output-context encoding and log control-character handling separately.

References