로그 인젝션

신뢰할 수 없는 텍스트가 로그 레코드 경계를 위조하지 못하도록 하는 방법

설명

로그 인젝션은 신뢰할 수 없는 텍스트를 출력 형식에 맞게 처리하지 않고 로그에 기록할 때 발생합니다. 줄 단위 텍스트 로그에서는 공격자가 입력에 캐리지 리턴(\r)이나 줄 바꿈(\n)을 넣어 하나의 이벤트를 여러 레코드처럼 보이게 만들 수 있습니다. CSV, JSON, HTML 또는 터미널에 표시되는 로그는 각 형식의 구분자와 제어 문자도 별도로 처리해야 합니다.

logger.info("login user={}", user)와 같은 매개변수화 로깅은 메시지 조합을 편리하게 하지만, 대입되는 값의 CR/LF를 자동으로 무해화한다는 보장은 없습니다. 실제 출력 형식과 인코딩 동작은 사용 중인 로깅 공급자, 레이아웃, 수집기 및 뷰어에 따라 달라집니다.

잠재적 영향

  • 감사 이벤트 위조 또는 실제 공격 흔적 은폐
  • 경보, 검색, 집계 및 포렌식 파이프라인의 레코드 구조 훼손
  • 로그 뷰어에서의 후속 인젝션이나 운영자 판단 오류
  • 과도하게 긴 입력을 함께 기록할 경우 저장 공간과 처리 자원 소모

길이 제한은 자원 소모를 줄이는 보조 수단일 뿐, 로그 레코드 인젝션을 방지하지는 않습니다.

해결 방법

  1. 가능하면 로그 값을 최종 출력 형식에 맞게 구조적으로 인코딩하는, 현재 지원되는 로깅 공급자와 레이아웃을 사용합니다. 공격 문자열을 포함한 테스트로 공급자부터 수집기와 뷰어까지 전체 경로의 동작을 확인합니다.
  2. 줄 단위 텍스트 로그라면 메시지와 모든 치환 인수에 포함된 \r과 \n을 각각 제거하거나 가시적인 안전한 표현으로 치환한 뒤 기록합니다. 자리표시자 사용만으로는 이 처리를 대체할 수 없습니다.
  3. 여러 호출 지점에서 같은 처리가 필요하면 검증된 중앙 헬퍼나 로깅 필터를 사용합니다. 값이 작은 폐쇄형 집합에 속하는 경우에는 변경 불가능한 리터럴 allowlist도 사용할 수 있습니다. 길이 제한은 레코드 구분자를 처리한 후 추가합니다.
  4. 비밀번호, 세션 식별자, 액세스 토큰과 같은 민감 정보는 로그에서 제외하고, 필요한 식별 정보만 일관되게 삭제하거나 마스킹합니다.

Apache Commons Text의 StringEscapeUtils.escapeJava는 CR/LF 같은 제어 문자를 Java 이스케이프 문자열로 바꾸므로 줄 단위 텍스트에서 레코드 분리를 막는 데 사용할 수 있습니다. 그러나 CSV, HTML, 터미널 등 다른 출력 문맥을 위한 범용 인코더는 아닙니다.

예시

변경 전

다음 코드는 자리표시자를 사용하지만 untrustedUser에 포함된 개행 문자를 그대로 로그 공급자에 전달합니다.

java
import jakarta.servlet.http.HttpServletRequest;
import java.util.logging.Level;
import java.util.logging.Logger;

final class VulnerableAuditLogExample {
    private static final Logger LOGGER =
            Logger.getLogger(VulnerableAuditLogExample.class.getName());

    static void recordFailedLogin(HttpServletRequest request) {
        String untrustedUser = request.getParameter("username");
        LOGGER.log(Level.WARNING, "Failed login for {0}", untrustedUser);
    }
}

변경 후

줄 단위 텍스트 로그에 기록하기 전에 모든 CR/LF를 제거합니다. 실제 애플리케이션에서는 이 처리를 중앙 헬퍼나 출력 레이아웃에 두는 것이 좋습니다.

java
import jakarta.servlet.http.HttpServletRequest;
import java.util.logging.Level;
import java.util.logging.Logger;

final class SafeAuditLogExample {
    private static final Logger LOGGER =
            Logger.getLogger(SafeAuditLogExample.class.getName());

    static void recordFailedLogin(HttpServletRequest request) {
        String untrustedUser = request.getParameter("username");
        String safeUser = untrustedUser == null ? "" : untrustedUser.replaceAll("[\\r\\n]", "");
        LOGGER.log(Level.WARNING, "Failed login for {0}", safeUser);
    }
}

replaceAll("\\r\\n", "")처럼 CRLF 쌍만 제거하거나 CR 또는 LF 중 하나만 제거하는 코드는 단독 개행 문자를 남기므로 충분하지 않습니다.

참조

지원 종료된 Log4j 1.x 대신 지원되는 로깅 라이브러리를 사용하세요.