포맷 문자열 취약점

외부 입력이 제어하는 포맷 문자열

설명

포맷 문자열 취약점은 String.format, System.out.printf, Formatter.format 등에서 포맷 문자열 자리에 신뢰할 수 없는 외부 입력을 그대로 사용할 때 발생합니다. 공격자는 추가 포맷 지정자(예: %s, %d, 위치 지정자 %1$... 등)를 삽입하여 인자 개수/타입 불일치를 유도해 예외를 발생시키거나, 위치 지정자를 악용해 해당 호출에 전달된 다른 인자의 내용을 의도하지 않게 노출시키는 등의 공격을 할 수 있습니다.

잠재적 영향

  • 서비스 장애: 잘못된 포맷 지정으로 MissingFormatArgumentException, IllegalFormatConversionException 등이 발생해 요청 처리 실패 또는 장애 유발
  • 정보 노출: 위치 지정자(%1$..., %2$...) 등을 악용해 해당 호출에 전달된 다른 인자의 값이 노출될 수 있음
  • 로그 오염/우회: %n 등 제어 문자를 삽입해 로그 포맷을 깨뜨리거나 다중 라인을 유도해 모니터링을 우회
  • 자원 고갈형 DoS: 매우 큰 폭/정밀도(%1000000s 등)를 사용해 과도한 메모리/CPU 사용을 유발할 가능성

해결 방법

  • 포맷 문자열은 항상 상수(고정 문자열)로 사용하고, 사용자 입력은 포맷 인자로만 전달하기
  • 입력을 그대로 출력해야 한다면 포맷 문자열로 "%s"를 사용하기
  • 포맷 패턴을 외부에서 받아야 한다면, 허용 목록(whitelist) 기반으로 제한된 소수 패턴만 허용하고 나머지는 기본 패턴으로 대체
  • 로그 출력에는 SLF4J의 매개변수 바인딩(예: logger.info("User: {}", user))을 사용하세요. 로그 줄바꿈 등 제어 문자 처리는 별도로 적용해야 합니다.

예시

변경 전

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);
    }
}

변경 후

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);
    }
}

설명:

  • 변경 전: 외부 입력값이 포맷 문자열로 그대로 사용됩니다. 이 예시에는 인자가 하나뿐이므로 %2$s는 인자 부족 예외를 일으키며, %d는 문자열 인자와 타입이 맞지 않습니다. 위치 지정자는 호출에 전달하지 않은 메모리를 읽는 기능이 아닙니다.
  • 변경 후: 포맷 문자열을 상수나 제한된 허용 목록으로 고정합니다. 사용자 입력은 포맷 인자로 전달하므로 입력 안의 포맷 지정자가 해석되지 않습니다. 출력 위치에 필요한 인코딩과 로그 제어 문자 처리는 별도로 적용하세요.

참조