개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › 보안 › 웹 공격과 방어

정보 누출

오류 응답과 로그인 실패 글귀는 무엇을 밖으로 알려 주나

서버가 실패했을 때 돌려주는 응답에는 생각보다 많은 것이 실릴 수 있습니다. 예외 메시지에 든 SQL 문장, 스택 트레이스에 든 클래스 이름과 줄 번호는 서버 안의 모습을 그대로 보여 주어 공격하는 사람에게 단서가 됩니다. 로그인 실패 글귀가 "없는 아이디"와 "틀린 비밀번호"를 구별해 주면 어떤 아이디가 가입돼 있는지도 알려 주게 됩니다.

실행으로 대조 · 스프링 부트 4.1.1(스프링 시큐리티 7.1.1) 서버의 실제 응답과 스무 가지 조합을 대조. 글귀가 아니라 항목이 들어 있는지와 두 응답이 같은지를 비교

이 실습에 나오는 개념

오류 응답과 정보 누출
서버가 실패했을 때 돌려주는 응답에 서버 안의 정보가 실려 밖으로 나가는 것이다. 예외 메시지에는 실행하려던 SQL이나 파일 경로가, 스택 트레이스에는 쓰는 프레임워크와 코드의 구조가 들어 있어 공격하는 사람에게 다음 시도를 위한 단서가 된다. 스프링 부트의 기본 오류 응답은 예외 메시지와 스택 트레이스를 넣지 않으며, 설정을 always로 바꾸거나 코드에서 예외의 메시지를 그대로 응답에 넣을 때 새어 나간다. 응답에는 정해 둔 글귀와 오류 번호만 담고, 자세한 내용은 서버의 로그에 남기는 것이 기본이다.
스택 트레이스
예외가 생긴 순간에 어떤 메서드들이 어떤 순서로 호출되고 있었는지를 적은 기록이다. 첫 줄에 예외의 클래스 이름과 메시지가 있고, 그 아래로 메서드마다 클래스 이름, 메서드 이름, 파일 이름과 줄 번호가 한 줄씩 이어지며, 원인이 된 다른 예외가 있으면 Caused by로 덧붙는다. 개발자가 오류의 원인을 찾을 때 가장 먼저 보는 자료다. 그만큼 프로그램 안의 구조를 자세히 보여 주므로 사용자에게 가는 응답에는 넣지 않고 로그에만 남긴다.
계정 열거
서버의 응답 차이를 이용해 어떤 아이디나 이메일이 가입돼 있는지를 알아내는 것이다. 로그인 실패 글귀가 "없는 아이디"와 "틀린 비밀번호"를 구별해 주거나, 회원 가입과 비밀번호 찾기 화면이 가입 여부를 알려 주면, 아이디를 바꿔 가며 요청해 가입된 아이디의 목록을 만들 수 있다. 이 목록은 비밀번호를 대입해 보거나 속이는 메일을 보낼 대상이 된다. 막으려면 두 경우의 응답을 글귀와 상태 코드까지 같게 만들고, 응답 시간의 차이도 줄인다. 스프링 시큐리티는 기본으로 두 경우를 같은 예외로 다룬다.
@ControllerAdvice와 @ExceptionHandler
여러 컨트롤러에서 발생한 예외를 한곳에서 처리하게 하는 기능이다. @ControllerAdvice 클래스 안에 예외 종류별로 @ExceptionHandler 메서드를 두면, 컨트롤러 실행 중 발생한 예외가 그 메서드로 전달되어 일관된 형식의 오류 응답을 만들 수 있다.
HTTP 상태 코드
서버가 요청을 처리한 결과를 나타내는 세 자리 숫자다. 2xx는 성공, 3xx는 다른 위치로의 이동(리다이렉션), 4xx는 클라이언트의 요청에 문제가 있음, 5xx는 서버 쪽 오류를 뜻한다. 200(성공), 201(생성됨), 400(잘못된 요청), 401(인증 필요), 403(권한 없음), 404(없음), 500(서버 오류)이 자주 쓰인다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. GET /orders/7 요청이 들어옵니다. 주문 7번을 조회하는 평범한 요청입니다.
  2. jdbc.queryForMap(SQL, id): SQL에 컬럼 이름 price가 pirce로 잘못 적혀 있습니다. DB가 이 SQL을 거절하고, 스프링이 그것을 BadSqlGrammarException으로 바꿔 던집니다. 이 예외의 메시지에는 실행하려던 SQL 문장이 통째로 들어 있습니다.
  3. 예외를 잡는 코드가 없으므로 요청이 /error로 다시 넘겨지고, 스프링 부트의 BasicErrorController가 오류 응답을 만듭니다. 설정과 관계없이 들어가는 항목은 timestamp, status, error, path입니다. 여기까지는 "오류가 났다"는 사실만 알려 줍니다.
  4. include-message=always: message 항목에 예외의 메시지를 넣습니다. SQL 문장이 그대로 응답에 실리고, 테이블 이름(orders)과 컬럼 이름(id, item)이 드러납니다.
  5. include-stacktrace=always: trace 항목에 스택 트레이스를 넣습니다. 첫 줄에 예외의 클래스 이름과 메시지(SQL 포함)가 있고, 그 아래로 예외가 지나온 메서드가 클래스 이름과 줄 번호까지 나열됩니다. DB 드라이버의 클래스 이름(org.h2…)에서 어떤 DB를 쓰는지도 보입니다.
  6. 서버가 500 응답을 보냅니다. 본문의 항목은 timestamp, status, error, trace, message, path입니다. 요청을 보낸 사람이 누구든 SQL 문장, 클래스 이름과 줄 번호, DB 제품 이름을 읽을 수 있습니다.

실행 결과

코드

# application.properties
spring.web.error.include-message=always
spring.web.error.include-stacktrace=always

// 주문 조회. SQL의 컬럼 이름이 틀렸다 (price를 pirce로 적음)
@GetMapping("/orders/{id}")
public Map<String, Object> order(@PathVariable long id) {
    return jdbc.queryForMap(
        "select id, item, pirce from orders where id = ?", id);
}

// 요청: GET /orders/7

이 모형의 전제

오류 응답 열두 가지(메시지 설정 2 × 스택 트레이스 설정 3 × trace 파라미터 유무)와 직접 만든 응답 두 가지, 로그인 실패 여섯 가지를 스프링 부트 4.1.1(스프링 시큐리티 7.1.1, 스프링 JDBC, H2) 서버를 127.0.0.1에 띄워 실제 응답과 대조했습니다. 응답의 글귀와 스택 트레이스의 줄 수는 버전과 환경에 따라 달라지므로, 대조한 것은 본문에 어떤 항목이 있는지, 본문에 SQL 문장과 스택 트레이스의 줄과 DB 드라이버의 이름이 보이는지, 두 로그인 실패 응답이 같은지, 어떤 예외가 났는지, 비밀번호 비교가 몇 번 불렸는지입니다. 화면의 message 값은 이 PC에서 본 것이고, errorId의 예는 꾸민 값입니다. JSON으로 받는 요청을 기준으로 했으며, 브라우저처럼 HTML을 받는 요청에는 같은 내용이 오류 화면에 실리는 것을 한 가지 설정에서 확인했습니다. 설정 이름은 스프링 부트 4.1.1의 spring.web.error.*를 썼습니다(스프링 부트 3까지는 server.error.*). include-exception, include-binding-errors와 message의 on_param 값은 다루지 않았습니다. 안전한 응답에서 예외 전체가 로그에 남는 것은 로그 기록을 모아 확인했습니다. 실행하지 않은 것: 응답 시간의 차이는 재지 않았고(비밀번호 비교가 불린 횟수만 확인), 회원 가입과 비밀번호 찾기 화면의 글귀는 OWASP의 인증 관련 안내를 따른 설명입니다. 새어 나간 정보가 실제 공격에 얼마나 쓰이는지는 서버마다 다릅니다.

앞 실습: 클릭재킹과 오픈 리다이렉트 · 다음 실습: 요청 수 제한

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요