설명
로그 인젝션은 신뢰할 수 없는 텍스트를 출력 형식에 맞게 처리하지 않고 로그에 기록할 때 발생합니다. 줄 단위 텍스트 로그에서는 공격자가 입력에 캐리지 리턴(\r)이나 줄 바꿈(\n)을 넣어 하나의 이벤트를 여러 레코드처럼 보이게 만들 수 있습니다. CSV, JSON, HTML 또는 터미널에 표시되는 로그는 각 형식의 구분자와 제어 문자도 별도로 처리해야 합니다.
logger.info("login user={}", user)와 같은 매개변수화 로깅은 메시지 조합을 편리하게 하지만, 대입되는 값의 CR/LF를 자동으로 무해화한다는 보장은 없습니다. 실제 출력 형식과 인코딩 동작은 사용 중인 로깅 공급자, 레이아웃, 수집기 및 뷰어에 따라 달라집니다.
잠재적 영향
- 감사 이벤트 위조 또는 실제 공격 흔적 은폐
- 경보, 검색, 집계 및 포렌식 파이프라인의 레코드 구조 훼손
- 로그 뷰어에서의 후속 인젝션이나 운영자 판단 오류
- 과도하게 긴 입력을 함께 기록할 경우 저장 공간과 처리 자원 소모
길이 제한은 자원 소모를 줄이는 보조 수단일 뿐, 로그 레코드 인젝션을 방지하지는 않습니다.
해결 방법
- 가능하면 로그 값을 최종 출력 형식에 맞게 구조적으로 인코딩하는, 현재 지원되는 로깅 공급자와 레이아웃을 사용합니다. 공격 문자열을 포함한 테스트로 공급자부터 수집기와 뷰어까지 전체 경로의 동작을 확인합니다.
- 줄 단위 텍스트 로그라면 메시지와 모든 치환 인수에 포함된
\r과\n을 각각 제거하거나 가시적인 안전한 표현으로 치환한 뒤 기록합니다. 자리표시자 사용만으로는 이 처리를 대체할 수 없습니다. - 여러 호출 지점에서 같은 처리가 필요하면 검증된 중앙 헬퍼나 로깅 필터를 사용합니다. 값이 작은 폐쇄형 집합에 속하는 경우에는 변경 불가능한 리터럴 allowlist도 사용할 수 있습니다. 길이 제한은 레코드 구분자를 처리한 후 추가합니다.
- 비밀번호, 세션 식별자, 액세스 토큰과 같은 민감 정보는 로그에서 제외하고, 필요한 식별 정보만 일관되게 삭제하거나 마스킹합니다.
Apache Commons Text의 StringEscapeUtils.escapeJava는 CR/LF 같은 제어 문자를 Java 이스케이프 문자열로 바꾸므로 줄 단위 텍스트에서 레코드 분리를 막는 데 사용할 수 있습니다. 그러나 CSV, HTML, 터미널 등 다른 출력 문맥을 위한 범용 인코더는 아닙니다.
예시
변경 전
다음 코드는 자리표시자를 사용하지만 untrustedUser에 포함된 개행 문자를 그대로 로그 공급자에 전달합니다.
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를 제거합니다. 실제 애플리케이션에서는 이 처리를 중앙 헬퍼나 출력 레이아웃에 두는 것이 좋습니다.
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 중 하나만 제거하는 코드는 단독 개행 문자를 남기므로 충분하지 않습니다.
참조
- OWASP Logging Cheat Sheet
- OWASP Top 10:2025 A09 - Security Logging and Alerting Failures
- OWASP Top 10:2021 A09 - Security Logging and Monitoring Failures
- OWASP ASVS 5.0.0 V16.4.1
- CWE-117: Improper Output Neutralization for Logs
- Java SE 25
java.util.logging.Logger - Java SE 25
String - SLF4J 2.0.18
Logger및LoggingEventBuilder - Apache Log4j 2.26.1
Logger및LogBuilder - Apache Commons Logging 1.4.0
Log - Apache Commons Text 1.15.0
StringEscapeUtils
지원 종료된 Log4j 1.x 대신 지원되는 로깅 라이브러리를 사용하세요.