Skip to content

CWE-134: Use of Externally-Controlled Format String - Java

Overview

Java's String.format(), Formatter, and PrintStream.printf() accept a format string containing conversion specifiers (%s, %d, %n, ...), and java.text.MessageFormat accepts a pattern in a different syntax ({0}, {1,number,integer}) that carries the same risk. Unlike C, Java's format specifiers cannot read or write arbitrary memory - %n produces the platform line separator, not a write primitive, and an out-of-range specifier throws a catchable exception rather than corrupting memory. The real risk in Java is denial of service: if untrusted input becomes the format string itself, an attacker can supply a specifier that doesn't match the arguments provided (%d with no numeric argument, or more specifiers than arguments) and trigger an unhandled IllegalFormatException/MissingFormatArgumentException that crashes the request or the thread. Where the call supplies more than one argument there is an information-disclosure path as well, through positional specifiers: %3$s prints the third argument whatever the application's own template showed, so a template the attacker writes can reach a value the intended one never printed.

Primary Defence: Keep the format string a compile-time literal and pass user data as arguments, e.g. String.format("%s", userInput) rather than String.format(userInput). For logging, use the logging framework's parameterized placeholders instead of building the format string from user input.

Common Vulnerable Patterns

User Input as the Format String

public class VulnerableFormat {
    public String formatMessage(String userTemplate, String username) {
        // VULNERABLE - userTemplate is attacker-controlled and used as the format string
        return String.format(userTemplate, username);
    }
}

// Attack: userTemplate = "%d" -> throws IllegalFormatConversionException (username isn't numeric)
// Attack: userTemplate = "%s %s %s" -> throws MissingFormatArgumentException (only one argument supplied)
// Result: an unhandled exception crashes the request or thread (denial of service)

Why this is vulnerable: String.format interprets whatever string it's given as a template. An attacker who controls the template controls how many arguments the call expects and how they're interpreted, and can reliably trigger an exception the calling code isn't prepared to catch.

User-Controlled MessageFormat Pattern

import java.text.MessageFormat;

public class VulnerableMessageFormat {
    public String render(String userPattern, Object... args) {
        // VULNERABLE - userPattern is attacker-controlled and used as the MessageFormat pattern
        return MessageFormat.format(userPattern, args);
    }
}

// Attack: userPattern = "hi {0 unmatched"
//   -> IllegalArgumentException: Unmatched braces in the pattern.
// Attack: userPattern = "{0,number,integer}" against a non-numeric argument
//   -> IllegalArgumentException: Cannot format given Object as a Number
// Attack: userPattern = "{2}" -> prints the third argument, whichever value that is

Why this is vulnerable: MessageFormat is Java's other externally-controlled template API, and it is the one likeliest to hold user input, because patterns legitimately live in resource bundles, databases and admin-editable settings rather than in source. Its syntax is {0}, not %s - a %d in the pattern is literal text here - but the consequences are the same two: a malformed or type-mismatched pattern throws IllegalArgumentException from a call the surrounding code has no reason to guard, and {2} selects an argument by index exactly as %3$s does. The exception is unchecked, so nothing in the signature suggests the call can fail.

Secure Patterns

Literal Format String, User Data as Argument

public class SecureFormat {
    public String formatMessage(String username) {
        // SECURE - format string is a compile-time literal
        return String.format("User: %s", username);
    }
}

Why this works: The format template can no longer be influenced by input - only the substituted value can be, so input cannot change how many arguments the call expects. %s accepts any object safely by calling its toString().

Parameterized Logging

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SecureLogging {
    private static final Logger logger = LoggerFactory.getLogger(SecureLogging.class);

    public void logUserAction(String username, String action) {
        // SECURE - {} placeholders are substituted, never interpreted as a format string
        logger.info("User {} performed action: {}", username, action);
    }
}

Why this works: SLF4J's {} placeholders are positional substitutions, not a format mini-language - there's no specifier syntax for user-controlled text to trigger, and the message template itself stays a compile-time literal, which is the part that matters. Keeping it literal rules out two other shapes: assembling the message with String.format first and passing the result in - logger.info(String.format(userTemplate, username)) is the vulnerable call at the top of this page with a logger in front of it - and handing a user-controlled message to a parameterized log call, which on java.util.logging makes it a MessageFormat pattern over the values you supplied. See Considerations for which flagged logging calls are real and which are not.

Considerations

A user-selected format may be a real product requirement. Letting someone choose how a date or number is displayed is a legitimate feature, and the fix is not to remove it. The answer is an allowlist: the user picks a key, the application holds the actual pattern strings, and no input reaches the formatter as a template. Note what the answer is not - wrapping the call in a try/catch for IllegalFormatException suppresses the symptom while leaving the attacker in control of the format, which is covered as a pitfall below.

Whether a flagged logging call is real depends on one thing: does the call pass parameters? Scanners flag logger.log(Level.INFO, userMessage) on sight, and the answer differs between its two overloads. Measured on JDK 26:

  • java.util.logging with no parameters is inert. The message is returned verbatim, so %d %s %n in it is printed as typed. % never means anything to this API in either case. SLF4J's {} is likewise positional substitution with no specifier grammar at all, and passes the message through untouched when no arguments accompany it.
  • java.util.logging with parameters is a real instance of this weakness. The message becomes a MessageFormat pattern, so a user-controlled one selects among the parameters the call site supplied: log(Level.INFO, "visible={2}", new Object[]{"first", "second", "SECRET"}) emits visible=SECRET. Any index works, not only the low ones. That is the same positional disclosure as %3$s, arriving through an API most reviewers read as a plain message. The failure mode differs from String.format in one way worth knowing: Formatter.formatMessage catches what MessageFormat raises, so a malformed pattern is not a denial of service - "x={0{" silently emits x=, losing the record's content with no exception and no log line about it.

So the sinks are String.format, Formatter, printf, MessageFormat, and Logger.log in its parameterized form. Record a no-parameter logging call as a false positive; treat a parameterized one as live and give it a literal message. What is worth fixing at either is the value reaching the log - a user-controlled message belongs in a parameter, not in the message string, for the reasons on CWE-117.

Testing

  • A template with more specifiers than arguments supplied (e.g. "%s %s" with one argument) - should not reach String.format with an attacker-controlled template at all after the fix.
  • A type-mismatched specifier (e.g. "%d" applied to a non-numeric value) - same expectation.
  • A pattern with unmatched braces ("{0 unmatched") against any MessageFormat call site - same expectation; MessageFormat raises IllegalArgumentException, not a format exception, so a catch (IllegalFormatException) elsewhere in the class will not have covered it.
  • A positional specifier reaching past the arguments the intended template printed ("%3$s" where the call passes three values) - should not be reachable; where it is, it prints the third argument regardless of what the application's template showed.
  • Confirm legitimate formatted output (dates, numbers, usernames) still renders correctly through every changed call site.
  • If a dynamic-format allowlist path exists, test a pattern outside the allowlist and confirm it's rejected, not passed through.

Common Pitfalls

  • Fixing the call named in the finding but leaving a sibling call in the same class untouched: Classes that format in more than one place (one per method, or a display path and an export path) often only have the flagged line fixed - a MessageFormat.format(userPattern, ...) or PrintWriter.printf(userTemplate, ...) a few lines below the corrected String.format call remains just as vulnerable, and the MessageFormat one throws a different exception type.
  • Treating %n as equivalent to C's %n: Java's %n is a harmless platform line-separator specifier, not a write primitive - don't carry over C-specific denylist logic (stripping %n specifically) into Java review; the actual Java risk is exception-based denial of service from any mismatched specifier, not memory corruption.
  • Catching the exception without fixing the root cause: Wrapping String.format(userTemplate, args) in a try/catch for IllegalFormatException stops the crash but still lets the attacker control the template - anything else the mismatched-specifier behavior can influence (garbled log output, an incomplete substitution silently swallowed, a %3$s reading an argument the template never showed) is still reachable. It also misses the cheapest denial of service outright: a width specifier such as %2000000000s asks for a two-billion-character result, which arrives as an OutOfMemoryError - measured on JDK 26 under -Xmx256m - and an OutOfMemoryError is an Error, not an IllegalFormatException. Make the format string a literal; don't just suppress the resulting exception.
  • Assuming resource-bundle message keys are always safe: A message loaded from a properties/resource bundle by a fixed key is safe as long as the key itself is fixed - if the key is chosen using untrusted input (letting an attacker select which bundle entry gets used as the format), the same risk reappears one level removed.

Additional Resources