외부 보안 점검을 받게 되어 OWASP Top 10 2017 항목을 미리 거래소 코드에 대 봤다. 몰라서 안 한 것도 있고 알면서 미뤄 둔 것도 있었다.
Table of contents
Open Table of contents
증상 — 화면에만 있던 권한 확인
출금 목록 조회에는 member_id = ? 조건이 들어가는데 GET /api/withdrawals/{id} 상세 조회는 식별자만 받고 있었다. 화면에서 자기 것만 보이니 다른 식별자를 알 방법이 없다고 전제한 구조였다.
식별자가 순서대로라 옆 번호를 넣으면 그대로 열렸다. A5 Broken Access Control 공식 문서가 「기본 키를 다른 사용자의 것으로 바꿔 남의 레코드를 본다」고 적은 바로 그 형태였다. 조회할 때마다 그것이 이 사용자의 것인지 WHERE 에 넣게 고쳤고 한 곳이 아니라 @GetMapping 전부를 찾아 고치는 데 시간이 걸렸다.
순서대로인 번호가 새는 것
순서대로인 식별자는 두 가지를 흘린다. 옆 번호가 존재한다는 것을 알려 주고 지금 번호로 전체 출금 건수를 짐작하게 한다.
그래서 내부 id 는 그대로 두고 밖으로 나가는 값만 IdObfuscator 로 다른 형태로 만들었다. 처음에는 단순한 덧셈 규칙으로 바꿨는데 몇 개를 비교하면 규칙이 드러나서 곱셈 역원 방식으로 다시 만들었다.
로그에 전체를 찍으면
logback 로그를 훑어보니 비밀번호는 가려져 있는데 계좌번호와 API 토큰이 그대로 남아 있었다. 응답 전체를 toString() 으로 찍는 코드가 여기저기 있었고 그 안에 무엇이 들어 있는지는 안 보고 있었다.
가려야 할 항목 목록을 만들어 나가기 전에 MaskingConverter 로 지우게 했다. 근본은 전체를 찍지 않는 것인데 전체를 찍으면 나중에 필드가 추가돼도 모른 채 함께 나간다.
판단 기준 — 친절한 오류가 정보를 준다
로그인 실패 메시지가 아이디가 없는 경우와 비밀번호가 틀린 경우로 나뉘어 있었다. 친절한 안내로 만든 것인데 그것으로 어떤 아이디가 가입돼 있는지를 가릴 수 있다.
두 가지 방법이 있었다. 메시지를 그대로 두고 시도 횟수 제한만 거는 것과, 메시지를 하나로 합치는 것이다. 뒤쪽을 골랐다. 이유는 횟수 제한은 속도를 늦출 뿐 구분 자체는 여전히 밖으로 나가서다. 대신 사용자에게는 불친절해지는 것을 감수했고 그 구분이 밖으로 나가지 않으므로 받아들일 만한 맞바꿈이었다. 시도 횟수도 세지 않고 있어서 A2 문서가 말하는 자격 증명 대입을 계속 시도할 수 있는 상태였다.
주의 — 잠그는 것 자체가 공격이 되는 자리
login_attempts 를 세어 일정 이상이면 잠그게 하려니 다른 문제가 보였다. 남의 아이디로 일부러 틀려서 잠그면 그 사람이 못 쓰게 된다.
그래서 계정 단위만이 아니라 요청이 오는 IP 단위로도 함께 셌다. 완전한 답은 아니라고 보고 그 사실을 함께 적었는데 pom.xml 의존성 취약점 확인과 application.yml 의 접속 정보 처리는 아직 못 한 것으로 남겼다.
정리
- 화면에서 안 보인다고 접근이 막힌 것이 아니다
- 조회할 때마다 그것이 이 사용자의 것인지
WHERE에 넣는다 - 순서대로인 식별자는 존재 여부와 규모를 함께 흘린다
- 밖으로 나가는 값만
IdObfuscator로 바꾸되 규칙이 드러나지 않게 한다 - 로그에 응답 전체를 찍으면 새 필드가 모른 채 함께 나간다
- 실패 메시지를 나누면 그 구분 자체가 정보가 된다
- 시도 횟수를 세되 잠그는 것이 공격이 되는 경우를 함께 본다
- 못 한 항목을 남겨서 다음에 무엇을 할지 남긴다