금융권 보안 - HTTP 응답 분할 (CRLF Injection)

헤더 인젝션 | 위험도: 4 (높음) | 세션 탈취, 쿠키 조작 가능

XX은행

인터넷뱅킹 - 사용자 설정

취약점: HTTP 헤더 검증 미흡

프로필 설정

언어 설정이 HTTP 쿠키로 저장됩니다

정상 요청

Set-Cookie: lang=ko-KR

CRLF 인젝션 공격 시나리오

CRLF 인젝션이란?

CRLF: Carriage Return + Line Feed (개행 문자)

\r\n = CR (0x0D) + LF (0x0A)
URL 인코딩: %0d%0a

⚠️ HTTP 헤더는 CRLF로 구분됩니다. 사용자 입력에 CRLF를 삽입하면 임의의 헤더를 추가할 수 있습니다.

취약한 Java 코드

위험
// ❌ 취약한 코드 - CRLF 검증 없음
@GetMapping("/setLang")
public void setLanguage(
    @RequestParam String lang,
    HttpServletResponse response) {
    
    // 사용자 입력을 검증 없이 헤더에 직접 사용!
    Cookie cookie = new Cookie("lang", lang);
    response.addCookie(cookie);
    
    // 또는
    response.setHeader("Set-Cookie", "lang=" + lang);
}

HTTP 응답 헤더

NORMAL
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Set-Cookie: lang=ko-KR
Content-Length: 2048
 
(Body content...)

안전한 Java 코드

권장
// ✅ 안전한 코드 - CRLF 필터링 및 검증
@GetMapping("/setLang")
public void setLanguage(
    @RequestParam String lang,
    HttpServletResponse response) 
    throws SecurityException {
    
    // 1. CRLF 문자 검증 (차단)
    if (lang.contains("\r") || lang.contains("\n")) {
        throw new SecurityException(
            "Invalid characters in language parameter"
        );
    }
    
    // 2. URL 인코딩된 CRLF 검증
    String decoded = URLDecoder.decode(lang, "UTF-8");
    if (decoded.contains("\r") || decoded.contains("\n")) {
        throw new SecurityException(
            "Encoded CRLF detected"
        );
    }
    
    // 3. 화이트리스트 검증 (권장)
    Set<String> allowedLanguages = Set.of(
        "ko-KR", "en-US", "ja-JP", "zh-CN"
    );
    
    if (!allowedLanguages.contains(lang)) {
        throw new IllegalArgumentException(
            "Unsupported language: " + lang
        );
    }
    
    // 4. 안전한 쿠키 설정
    Cookie cookie = new Cookie("lang", lang);
    cookie.setHttpOnly(true);  // XSS 방어
    cookie.setSecure(true);    // HTTPS만
    cookie.setPath("/");
    cookie.setMaxAge(86400);   // 24시간
    
    response.addCookie(cookie);
}

// 추가: ResponseEntity 사용 (Spring 권장)
@GetMapping("/setLang")
public ResponseEntity<String> setLanguage(
    @RequestParam String lang) {
    
    // CRLF 검증
    if (lang.matches(".*[\\r\\n].*")) {
        return ResponseEntity
            .badRequest()
            .body("Invalid parameter");
    }
    
    // 화이트리스트 검증
    if (!ALLOWED_LANGS.contains(lang)) {
        return ResponseEntity
            .badRequest()
            .body("Unsupported language");
    }
    
    // ResponseEntity로 안전하게 반환
    return ResponseEntity
        .ok()
        .header(HttpHeaders.SET_COOKIE, 
                "lang=" + lang + "; HttpOnly; Secure")
        .body("Language set to: " + lang);
}

// 정규식 기반 필터링
private boolean isValidInput(String input) {
    // CRLF 및 특수 문자 검증
    Pattern pattern = Pattern.compile("^[a-zA-Z0-9-_]+$");
    return pattern.matcher(input).matches();
}

// Apache Commons 사용
import org.apache.commons.text.StringEscapeUtils;

String sanitized = StringEscapeUtils.escapeHtml4(userInput);
response.setHeader("Custom-Header", sanitized);

CRLF 인젝션 방어 체크리스트

CRLF 문자 차단: \r \n %0d %0a 필터링
화이트리스트: 허용된 값만 사용
정규식 검증: 영숫자만 허용
헤더 API 사용: response.addCookie() 등
입력 길이 제한: 최대 50자 이하