HTTP 응답 분할 취약점이 발생하는 원인과 CRLF 문자가 응답 헤더를 조작하는 과정, 취약한 Java 코드 및 안전한 예방 방법을 알아봅니다.
웹 서버가 브라우저에 보내는 HTTP 응답은 상태 코드, 응답 헤더와 본문으로 구성된다. 브라우저는 응답에 포함된 줄바꿈 문자를 기준으로 각 헤더를 구분하고, 빈 줄이 나오면 이후 내용을 본문으로 처리한다.
이때 사용자가 입력한 값이 응답 헤더에 그대로 포함되면 공격자가 줄바꿈 문자를 삽입해 새로운 헤더를 추가하거나 응답 구조를 변경할 수 있다. 이러한 보안 문제를 HTTP 응답 분할 취약점이라고 한다.
CRLF란?
CRLF는 ‘Carriage Return’과 ‘Line Feed’를 합친 표현이다. 프로그램에서는 각각 \r과 \n으로 표시하며, HTTP/1.1 메시지에서 한 줄이 끝났다는 것을 나타내는 데 사용된다.
HTTP 응답은 다음과 같이 상태 표시줄과 여러 헤더가 줄 단위로 구성된다.
HTTP/1.1 302 Found
Location: /home
Content-Type: text/html
응답 본문
실제 통신에서는 각 줄의 끝에 CRLF가 들어간다. 헤더가 끝난 뒤 연속된 CRLF가 나타나면 브라우저나 중간 장비는 그다음 내용을 응답 본문으로 해석한다.
CRLF는 정상적인 HTTP 통신에 반드시 필요한 구분자다. 문제는 외부 사용자가 입력한 값에 CRLF가 포함되고, 서버가 이를 일반적인 데이터가 아닌 헤더 구분자로 처리할 때 발생한다.
HTTP 응답 분할 취약점이란?
HTTP 응답 분할은 사용자가 입력한 CRLF 문자가 서버 응답의 구조를 변경하는 취약점이다. 공격자는 정상적인 헤더 값을 중간에서 끝내고 새로운 응답 헤더나 본문이 시작되는 것처럼 만들려고 시도한다.
예를 들어 로그인 후 이동할 주소를 Location 헤더에 넣거나, 다운로드 파일명을 Content-Disposition 헤더에 넣는 기능이 있다. 해당 값을 검증하지 않고 응답 헤더에 전달하면 CRLF 삽입 가능성이 생긴다.
고전적인 HTTP 응답 분할 공격에서는 하나의 서버 응답이 두 개의 응답처럼 해석되도록 조작될 수도 있다. 웹 브라우저뿐 아니라 프록시와 캐시 서버가 변조된 응답을 잘못 처리하면 다른 사용자에게 영향을 줄 가능성도 있다.
HTTP 응답 분할의 핵심은 외부 입력값이 응답 헤더의 값이 아니라 HTTP 메시지의 구분자로 해석된다는 점이다.
CRLF 삽입 취약점이 발생하는 원인
가장 흔한 원인은 URL 파라미터, 쿠키 또는 HTTP 헤더에서 받은 값을 응답 헤더에 직접 사용하는 것이다. 리다이렉트 주소, 파일명, 언어 설정, 사용자 이름과 추적용 식별자가 대표적이다.
개발자는 사용자가 입력한 문자열이 헤더 값 하나로 처리될 것이라고 생각할 수 있다. 하지만 사용 중인 서버나 프레임워크가 줄바꿈 문자를 차단하지 않는다면 입력값이 새로운 헤더 줄로 해석될 수 있다.
URL에서는 CR과 LF가 인코딩된 형태로 전달될 수 있다. 애플리케이션이 입력값을 디코딩한 뒤 검사하지 않거나 여러 단계에서 반복적으로 디코딩하면 필터를 통과한 값이 나중에 CRLF 문자로 바뀔 수 있다.
특정 인코딩 문자열만 차단하는 방식도 충분하지 않다. 입력값이 어느 단계에서 어떤 문자로 변환되는지 확인하고, 최종적으로 응답 헤더에 들어가는 값에 실제 CR 또는 LF 문자가 포함됐는지 검사해야 한다.
HTTP 응답 분할에 취약할 수 있는 Java 코드
다음은 로그인 후 사용자가 지정한 주소로 이동시키는 코드다.
@GetMapping("/move")
public void move(
@RequestParam String next,
HttpServletResponse response)
throws IOException {
response.setStatus(
HttpServletResponse.SC_FOUND
);
response.setHeader(
"Location",
next
);
}
이 코드는 next 값을 검증하지 않고 Location 응답 헤더에 전달한다. 사용자가 입력한 값에 줄바꿈 문자가 포함돼 있고 웹 서버가 이를 차단하지 않는다면 새로운 응답 헤더가 삽입될 가능성이 있다.
최근의 주요 서블릿 컨테이너와 웹 프레임워크는 헤더 값에 CR과 LF가 포함되면 예외를 발생시키거나 요청을 거부하는 경우가 많다. 따라서 위 코드가 모든 최신 환경에서 곧바로 응답 분할로 이어지는 것은 아니다.
하지만 프레임워크의 보호 기능만 믿어서는 안 된다. 오래된 서버, 자체 개발한 HTTP 처리 코드, 프록시와 게이트웨이를 거치는 환경에서는 동작이 달라질 수 있다. 또한 검증되지 않은 next 값은 CRLF가 차단되더라도 오픈 리다이렉트 취약점으로 이어질 수 있다.
CRLF 삽입 공격의 처리 과정
공격자는 응답 헤더에 반영되는 파라미터를 찾은 뒤 해당 값에 줄바꿈 문자가 포함되도록 요청을 조작한다. 서버가 입력값을 디코딩하고 별도의 검증 없이 헤더에 넣으면 줄바꿈 이후의 문자열이 새로운 헤더로 인식될 수 있다.
공격이 가능한 환경에서는 캐시 제어, 콘텐츠 유형 또는 쿠키와 관련된 헤더가 의도하지 않게 추가될 수 있다. 헤더와 본문의 경계까지 변경되면 공격자가 작성한 내용이 서버 응답 본문처럼 표시될 가능성도 있다.
프록시나 캐시 서버가 조작된 응답을 정상적인 콘텐츠로 저장하면 이후 같은 주소를 요청한 다른 사용자에게 변조된 결과가 전달될 수 있다. 이를 웹 캐시 포이즈닝으로 확대될 수 있는 위험이라고 볼 수 있다.
실제 영향은 웹 서버, 브라우저, 프록시 및 캐시 장비가 응답을 해석하는 방식에 따라 달라진다. 여러 장비가 HTTP 메시지의 경계를 서로 다르게 판단할수록 예상하지 못한 문제가 발생할 가능성이 커진다.
HTTP 응답 분할로 발생할 수 있는 피해
공격자가 새로운 응답 헤더를 삽입할 수 있다면 브라우저의 동작이나 캐시 정책이 변경될 수 있다. 보안 관련 헤더가 덮어써지거나 잘못된 콘텐츠 유형이 지정될 가능성도 있다.
응답 본문을 조작할 수 있는 환경에서는 허위 안내문이나 피싱 화면이 정상적인 사이트의 응답처럼 표시될 수 있다. 삽입된 내용이 브라우저에서 스크립트로 처리되면 XSS로 이어질 가능성도 있다.
캐시 서버가 변조된 응답을 저장하면 공격 요청을 보내지 않은 다른 사용자에게까지 영향을 줄 수 있다. 로그인이나 결제와 관련된 응답이 잘못 캐시될 경우에는 민감한 정보가 노출될 위험도 있다.
줄바꿈 문자가 응답 헤더가 아닌 로그에 삽입되면 로그 위조 문제가 발생할 수도 있다. 이는 HTTP 응답 분할과 발생 위치는 다르지만 CRLF 입력값을 제대로 처리하지 않았다는 공통점이 있다.
허용 목록을 이용한 안전한 리다이렉트 코드
가장 안전한 방법은 사용자가 전체 이동 주소를 직접 입력하지 못하게 하는 것이다. 서버에서 허용할 페이지와 실제 경로를 미리 연결하고 사용자는 페이지 식별자만 전달하도록 구성할 수 있다.
private static final Map<String, String>
ALLOWED_PAGES = Map.of(
"home", "/",
"profile", "/profile",
"support", "/support"
);
@GetMapping("/move")
public void move(
@RequestParam String page,
HttpServletResponse response)
throws IOException {
String target =
ALLOWED_PAGES.getOrDefault(
page,
"/"
);
response.sendRedirect(
response.encodeRedirectURL(target)
);
}
이 코드는 사용자가 입력한 값을 Location 헤더에 직접 사용하지 않는다. 사용자는 home, profile과 같은 식별자만 전달하고 실제 경로는 서버가 허용 목록에서 선택한다.
리다이렉트 대상이 서버 내부의 고정된 경로로 제한되므로 CRLF 삽입과 오픈 리다이렉트 위험을 함께 줄일 수 있다.
업무상 외부 URL을 허용해야 한다면 표준 URL 파서를 사용해 프로토콜, 호스트와 포트를 분리한 뒤 허용된 HTTPS 도메인만 통과시켜야 한다. 사용자 정보 영역이나 예상하지 못한 포트가 포함된 URL은 거부하는 것이 좋다.
응답 헤더 값 검증 방법
외부 입력값을 응답 헤더에 넣어야 한다면 최종 값에 \r 또는 \n이 포함돼 있는지 확인하고 발견 즉시 요청을 거부해야 한다. 단순히 해당 문자를 삭제한 뒤 계속 처리하는 방식보다 오류로 중단하는 편이 안전하다.
입력값은 정해진 방식으로 한 번만 디코딩하고, 검증을 마친 뒤 다시 해석되지 않도록 해야 한다. 여러 프레임워크와 프록시가 값을 반복해서 변환하면 검증 시점의 값과 실제 헤더에 들어가는 값이 달라질 수 있다.
헤더별로 허용할 값의 형식도 제한해야 한다. 언어 코드는 영문자와 하이픈, 파일명은 정해진 길이와 허용 문자처럼 업무에 필요한 범위를 명확하게 정하는 것이 좋다.
사용자 입력으로 헤더 이름을 결정하게 해서는 안 된다. 헤더 이름과 설정 가능한 항목은 서버에서 고정하고 사용자 입력은 제한된 값으로만 사용해야 한다.
안전한 다운로드 파일명 처리
파일 다운로드 기능에서는 원본 파일명을 Content-Disposition 헤더에 문자열로 직접 연결하는 경우가 많다. 파일명에 따옴표나 줄바꿈 문자가 포함되면 헤더 구조에 영향을 줄 수 있다.
Spring의 ContentDisposition처럼 표준 형식으로 헤더 값을 생성해 주는 기능을 사용하는 것이 좋다.
ContentDisposition disposition =
ContentDisposition
.attachment()
.filename(
safeFileName,
StandardCharsets.UTF_8
)
.build();
return ResponseEntity.ok()
.header(
HttpHeaders.CONTENT_DISPOSITION,
disposition.toString()
)
.body(resource);
safeFileName은 서버에서 관리하는 값이나 검증된 파일명이어야 한다. 경로 정보와 CRLF 문자를 제거하고 길이를 제한해야 한다. 가능하면 실제 저장 파일명은 UUID로 관리하고, 화면에 표시할 원본 이름은 데이터베이스에 별도로 보관하는 것이 안전하다.
프레임워크와 중간 장비 점검
최신 프레임워크가 CRLF를 차단하더라도 사용 중인 웹 서버와 프록시가 모두 같은 방식으로 값을 처리하는지 확인해야 한다. 애플리케이션, 웹 서버, 로드밸런서와 CDN을 거치는 동안 헤더가 디코딩되거나 다시 구성될 수 있기 때문이다.
웹 서버와 프레임워크는 최신 보안 버전으로 유지해야 한다. 헤더 검증 기능을 임의로 우회하는 설정이나 직접 원시 HTTP 응답을 작성하는 코드는 피하는 것이 좋다.
HTTP/2와 HTTP/3은 HTTP/1.1과 다른 방식으로 헤더를 전송하지만 중간 프록시에서 HTTP/1.1로 변환될 수 있다. 따라서 최신 프로토콜을 사용한다는 이유만으로 CRLF 검증을 생략해서는 안 된다.
HTTP 응답 분할 취약점 점검 항목
소스코드 점검에서는 setHeader(), addHeader(), sendRedirect()와 같이 응답 헤더를 설정하는 함수를 확인한다. 외부 입력값이 Location, Content-Disposition, Set-Cookie 등의 헤더에 전달되는지 추적해야 한다.
URL 파라미터, 요청 헤더, 쿠키와 데이터베이스에 저장된 값도 점검 대상이다. 과거에 저장된 사용자 입력값이 나중에 응답 헤더에 사용되는 저장형 CRLF 삽입이 발생할 수 있기 때문이다.
입력값을 검사한 뒤 다른 단계에서 다시 URL 디코딩하는 코드가 없는지도 확인한다. 사용자 입력값으로 응답 헤더 이름이나 전체 리다이렉트 주소를 결정하지 않는지 살펴봐야 한다.
운영 환경에서는 비정상적인 줄바꿈이 포함된 요청, 응답 헤더 생성 오류와 캐시 서버의 예상하지 못한 응답을 모니터링해야 한다.
자주 묻는 질문
%0D%0A는 무엇을 의미하나요?
URL 인코딩에서 %0D는 CR, %0A는 LF를 의미한다. 애플리케이션이 이를 디코딩하면 HTTP의 줄바꿈 문자가 될 수 있으므로 디코딩이 끝난 실제 값을 기준으로 검사해야 한다.
최신 웹 프레임워크를 사용하면 안전한가요?
주요 프레임워크는 헤더 값의 CRLF를 차단하는 경우가 많지만 이것만으로 안전하다고 단정할 수 없다. 프록시, 자체 HTTP 코드와 반복 디코딩 과정도 확인하고 외부 입력값을 헤더에 직접 사용하지 않는 구조로 만들어야 한다.
HTTP 응답 분할과 오픈 리다이렉트는 같은 취약점인가요?
같은 취약점은 아니다. HTTP 응답 분할은 CRLF를 이용해 응답 구조를 변경하는 문제이고, 오픈 리다이렉트는 사용자가 이동할 외부 주소를 결정하는 문제다. 검증되지 않은 Location 헤더에서는 두 취약점이 함께 발생할 수 있다.
CR과 LF를 삭제하면 충분한가요?
삭제보다 해당 입력을 거부하는 것이 안전하다. 값이 다른 단계에서 다시 디코딩될 가능성이 있으므로 표준화와 디코딩을 마친 최종 값에서 CRLF를 확인하고, 헤더별 허용 형식도 함께 검증해야 한다.
마무리
HTTP 응답 분할 취약점은 사용자가 입력한 CRLF 문자가 응답 헤더의 구분자로 처리될 때 발생한다. 공격자는 이를 이용해 새로운 헤더를 삽입하거나 응답 본문과 캐시 동작을 변경하려고 시도할 수 있다.
가장 안전한 대응 방법은 외부 입력값을 응답 헤더에 직접 사용하지 않는 것이다. 리다이렉트와 파일 다운로드 기능에서는 서버가 관리하는 허용 목록과 표준 헤더 생성 API를 사용해야 한다.
HTTP 응답 분할 방어의 핵심은 사용자 입력이 HTTP 응답 구조를 결정하지 못하게 하는 것이다. CRLF 검증, 허용 목록, 단일 디코딩과 최신 프레임워크를 함께 적용해야 응답 헤더 조작과 캐시 오염 위험을 줄일 수 있다.
핵심 키워드: HTTP 응답 분할, CRLF 삽입, CRLF Injection, HTTP 헤더 인젝션, 응답 헤더 조작, 웹 캐시 포이즈닝, CRLF 취약점, HTTP 응답 분할 대응