설명
Netty에서 HTTP 헤더 검증을 끄면 실제 CR/LF(\r\n) 문자가 포함된 값을 헤더에 넣을 수 있습니다. 신뢰할 수 없는 값이 이 경로로 응답이나 요청에 기록되고 수신자가 이를 메시지 경계로 해석하면 헤더 주입이나 HTTP 요청·응답 분할로 이어질 수 있습니다.
문자열 %0d%0a 자체는 개행이 아닙니다. URL 디코딩 등으로 실제 CR/LF가 된 값이 헤더에 도달해야 하며, 영향은 Netty 파이프라인과 프록시·수신자의 처리 방식에 따라 달라집니다.
잠재적 영향
- 추가 헤더를 통한 쿠키 설정이나 리다이렉트 조작
- 수신자의 해석에 따른 응답 콘텐츠 변조, XSS 또는 캐시 오염
- 요청 분할이 가능한 구성에서 의도하지 않은 백엔드 요청 실행
해결 방법
- 기본 생성자나 검증을 활성화하는 구성을 사용하세요. Netty 4.1의
DefaultHttpHeaders(boolean)은 사용을 권장하지 않으며 기본 생성자를 권장합니다. - 헤더 이름은 개발자가 관리하고, 값은 해당 헤더의 문법과 길이 제한에 맞게 검증하세요. CR/LF를 거부하거나 필드 의미를 해치지 않는 방식으로 처리하세요. 콜론은 모든 헤더 값에서 금지되는 문자가 아닙니다.
- 디코딩이 필요한 값은 최종 디코딩 이후 검증하고, 검증된 값을 다시 디코딩하지 마세요. 프록시의 보호 기능도 함께 확인하되 애플리케이션의 헤더 검증을 대체하지 마세요.
예시
다음은 Netty 4.1 헤더 처리 발췌입니다. name 쿼리 값을 URL 디코딩하여 사용하며, 전체 응답 본문과 연결 처리는 생략했습니다.
변경 전
java
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
public class UnsafeHeaderHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
// URL 디코딩 후 실제 CR/LF를 포함할 수 있는 쿼리 값
String clientName = new QueryStringDecoder(req.uri()).parameters()
.getOrDefault("name", java.util.List.of("")).get(0);
// BAD: 헤더 검증 비활성화
DefaultHttpResponse resp = new DefaultHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, false);
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8");
// BAD: 입력값을 검증 없이 응답 헤더에 복사 -> CRLF 주입 가능
resp.headers().set("X-Client-Name", clientName);
ctx.writeAndFlush(resp);
}
}
변경 후
java
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
public class SafeHeaderHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private static String cleanHeaderValue(String v) {
if (v == null) return "";
// CR/LF 제거 및 길이 제한
String cleaned = v.replace("\r", "").replace("\n", "");
return cleaned.length() > 128 ? cleaned.substring(0, 128) : cleaned;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
String clientName = cleanHeaderValue(new QueryStringDecoder(req.uri()).parameters()
.getOrDefault("name", java.util.List.of("")).get(0));
// GOOD: 기본 생성자(검증 활성화)
DefaultHttpResponse resp = new DefaultHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
HttpHeaders headers = resp.headers();
headers.set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8");
headers.set("X-Client-Name", clientName);
ctx.writeAndFlush(resp);
}
}
변경 후에는 헤더 검증을 유지하고, 디코딩된 값의 CR/LF와 길이를 처리합니다. 실제 헤더의 의미에 따라 잘못된 값을 수정하는 대신 요청을 거부할 수도 있습니다.