안전하지 않은 설계
고친 사람 github-actions[bot]
안전하지 않은 설계는 공격을 막는 장치가 설계에 처음부터 없어서 생긴 취약점입니다. 막아야 할 공격을 생각하지 않은 채 기능을 정했기 때문에, 코드를 짜기도 전에 뚫릴 길이 정해집니다. 그러면 코드를 아무리 꼼꼼하게 짜도 공격이 통합니다. 고치려면 코드 한 줄이 아니라 기능의 흐름을 다시 짜야 합니다.
쉽고 빠른 이해
안전하지 않은 설계는 막는 장치가 애초에 계획에 없어서 생기는 취약점입니다. 예를 들어 비밀번호를 잊은 사람에게 네 자리 번호를 문자로 보내는 서비스가 있습니다. 그 번호를 몇 번 틀려도 막지 않는다면 이 취약점이 있는 것입니다. 그 번호는 0000부터 9999까지 1만 가지뿐이라 다 넣어 보면 언젠가 맞기 때문입니다.
따로 이름을 붙이는 까닭은 코드를 들여다봐서는 안 보이기 때문입니다. 코드 리뷰와 테스트는 코드가 계획대로 도는지를 봅니다. 계획에 없는 장치는 아무도 찾지 않습니다.
어떻게 생기나:
- 기능을 정할 때 무엇을 해야 하는지만 적습니다
- 그 기능을 누가 악용할지는 묻지 않습니다
- 막는 장치가 없는 채로 코드가 계획대로 완성됩니다
언제 생기나: 기능을 악용할 길이 있고, 그 길을 막는 장치가 설계에 없고, 공격자가 그 기능에 닿을 수 있을 때입니다. 셋 중 하나라도 빠지면 생기지 않습니다.
무엇이 나빠지나: 고치는 일이 한 줄로 안 끝납니다. 흐름에 단계를 더해야 해서 이미 내보낸 기능을 다시 짜게 됩니다.
상세
이 절은 비밀번호 재설정 기능 하나를 끝까지 들고 갑니다. 먼저 설계가 무엇이고 거기서 어떻게 약점이 생기는지 봅니다. 그다음 그 기능이 실제로 뚫리는 경로를 따라갑니다. 끝으로 생기는 조건과 까닭, 이웃한 취약점과 가르는 선, 막는 방법을 차례로 봅니다.
설계에서 생기는 취약점
은행이 금고 문을 두껍게 만들었습니다. 그런데 설계도에는 직원 출입문이 자물쇠 없이 그려져 있습니다. 시공 팀이 설계도를 한 치도 안 틀리고 따르면 그 문은 잠기지 않는 문으로 완성됩니다. 시공을 잘할수록 구멍도 설계도대로 정확하게 생깁니다.
소프트웨어에서 설계는 코드를 짜기 전에 기능의 흐름과 규칙을 정하는 일입니다. 화면이 몇 단계로 넘어가는지, 무엇을 몇 번까지 받아 주는지, 누가 보낸 값을 믿는지가 여기서 정해집니다. 코드는 이렇게 정한 것을 옮기는 일입니다.
공격을 막으려고 흐름에 넣는 장치를 보호 장치라 부릅니다. 보안 분야에서는 보안 통제라고도 합니다. 틀린 횟수를 세어 끊는 규칙, 중요한 일 앞에 본인인지 한 번 더 묻는 단계, 요청한 사람에게 권한이 있는지 보는 검사가 보호 장치입니다.
공격자가 파고들 수 있는 약한 대목을 취약점이라 합니다. 취약점은 어느 단계에서 생겼느냐로 나눠 볼 수 있습니다. 보호 장치가 설계에 처음부터 없어서 생긴 취약점이 안전하지 않은 설계입니다.
OWASP(Open Worldwide Application Security Project)는 웹 애플리케이션이 자주 뚫리는 방식 열 가지를 목록으로 펴내는 단체입니다. 이 목록이 OWASP Top 10입니다. 안전하지 않은 설계는 이 목록에 한 항목으로 올라 있습니다. 이 항목은 무엇이 약하냐가 아니라 약점이 생긴 단계를 이름으로 삼았습니다.
비밀번호 재설정으로 본 설계 결함
어느 서비스가 비밀번호를 잊은 사용자를 위해 재설정 기능을 설계했습니다. 사용자가 전화번호를 넣으면 서버가 네 자리 숫자를 문자로 보냅니다. 사용자가 그 숫자를 넣으면 새 비밀번호를 정할 수 있습니다. 설계 문서에 적힌 것은 이 세 단계가 전부입니다.
개발자는 설계대로 번호를 확인하는 함수를 짰습니다. 저장해 둔 번호와 사용자가 넣은 번호를 견주는 함수입니다.
def check_code(phone, code):
saved = codes.get(phone) # "4831"
return code == saved # 맞으면 True
이 함수에는 버그가 없습니다. 맞는 번호는 통과시킵니다. 틀린 번호는 막습니다. 설계 문서가 시킨 일을 빠짐없이 합니다.
문제는 설계 문서에 틀린 횟수를 세라는 규칙이 없다는 것입니다. 네 자리 숫자는 0000부터 9999까지 1만 가지입니다. 서버는 틀린 번호가 몇 번 들어와도 매번 다시 받아 줍니다.
공격자는 남의 전화번호로 재설정을 요청합니다. 문자는 피해자 휴대폰으로 갑니다. 번호는 피해자에게 갔지만 공격자는 몰라도 됩니다. 0000부터 차례로 넣어 보면 1만 번 안에 반드시 맞기 때문입니다. 프로그램으로 보내면 1만 번은 금방 끝납니다.
sequenceDiagram
participant 공격자
participant 서버
공격자->>서버: 남의 전화번호로 재설정 요청
Note over 서버: 네 자리 번호를 피해자 휴대폰으로 보낸다
loop 0000부터 9999까지
공격자->>서버: 번호 하나를 넣어 본다
서버-->>공격자: 틀렸다
end
Note over 공격자,서버: 맞는 번호가 나오면 되풀이가 멈춘다
공격자->>서버: 새 비밀번호를 정한다
서버-->>공격자: 계정이 공격자에게 넘어간다
그림의 되풀이 구간에서 공격자가 한 일은 번호를 넣는 것뿐입니다. 서비스가 모든 사용자에게 열어 둔 기능을 정해진 방법대로 썼습니다. 이상한 문자를 섞지 않았습니다. 코드의 버그를 찌르지도 않았습니다.
이렇게 가능한 값을 처음부터 하나씩 다 넣어 보는 공격을 무차별 대입 공격이라 합니다. 경우의 수가 적은 값을 횟수 제한 없이 받아 주면 이 공격은 언젠가 반드시 성공합니다.
한 줄의 규칙이 바꾸는 것
이제 설계 문서에 한 줄을 더합니다. 「서버가 보낸 번호 하나에 틀린 입력은 다섯 번까지만 받고, 그 뒤에는 그 번호를 버린다」입니다. 이렇게 틀린 입력을 몇 번까지 받을지 정하는 규칙을 시도 횟수 제한이라 부릅니다.
이 규칙이 있으면 공격자는 보낸 번호 하나에 다섯 번만 넣어 볼 수 있습니다. 1만 가지 중 다섯 가지이니 그 번호를 맞힐 확률은 2천분의 1로 떨어집니다. 다만 이 확률은 번호 하나에 대한 것입니다. 재설정을 다시 요청하면 새 번호로 다섯 번을 또 얻으므로, 요청 횟수도 함께 막아야 합니다.
이 한 줄은 코드를 고쳐서 생기지 않습니다. 정해야 할 것이 먼저 여럿 있기 때문입니다.
- 틀린 횟수를 어디에 저장하나
- 몇 번에서 번호를 버리나
- 재설정 요청은 몇 번까지 받아 주나
- 더 시도하지 못하게 막힌 사용자에게 무엇을 보여 주나
- 막힌 사용자가 다시 시도하려면 어떻게 하나
이것들을 정하는 일이 설계입니다. 정해진 것이 없으면 개발자는 짤 것이 없습니다. 그래서 코드 리뷰도 테스트도 이 빈칸을 보지 못합니다.
생기는 조건
안전하지 않은 설계는 세 조건이 겹칠 때 생깁니다. 셋 중 하나라도 빠지면 생기지 않습니다.
- 기능을 악용할 길이 있습니다. 재설정 번호를 맞히면 남의 계정을 얻는 것이 그렇습니다
- 그 길을 막는 보호 장치가 설계에 없거나, 있어도 그 길을 막지 못합니다
- 공격자가 그 기능에 닿아 정상 사용자처럼 쓸 수 있습니다
세 번째 조건 때문에 같은 흐름도 어디에 열리느냐에 따라 결과가 갈립니다. 재설정 기능이 회사 안 네트워크에서만 열린다면 바깥 공격자는 이 기능에 닿지 못합니다. 같은 기능이 인터넷에 열리는 순간 세 조건이 다 찹니다.
두 번째 조건의 「있어도 막지 못한다」는 보호 장치를 엉뚱한 곳에 둔 경우입니다. 예를 들어 틀린 횟수를 서버가 아니라 사용자의 브라우저 쪽 쿠키에 적어 둡니다. 공격자는 쿠키를 지우고 처음부터 다시 셉니다. 장치는 설계에 있습니다. 그런데 공격자가 손댈 수 있는 곳에 있어서 아무것도 막지 못합니다.
이 경우의 바탕에는 누가 보낸 값을 믿을지에 대한 판단이 있습니다. 내가 통제하는 서버와 남이 통제하는 사용자 기기 사이의 선을 신뢰 경계라 합니다. 경계 바깥에서 온 값은 공격자가 마음대로 바꿀 수 있습니다. 그래서 그런 값에 보호 장치를 맡기면 장치가 장치 노릇을 못 합니다.
설계에서 보호 장치가 빠지는 까닭
기능을 정하는 문서는 대개 시스템이 해야 할 일을 적습니다. 「사용자는 비밀번호를 재설정할 수 있다」 같은 문장입니다. 무엇을 거부해야 하는지는 잘 적히지 않습니다.
정상 사용자가 기능을 쓰는 흐름을 유스케이스라 부릅니다. 유스케이스만 보고 설계하면 정상 사용자의 길만 다듬어집니다. 공격자가 같은 기능을 어떻게 쓸지는 아무도 그려 보지 않은 채 남습니다.
그래서 기능마다 악용하는 흐름을 따로 적는 방법이 있습니다. 이 흐름을 오용 사례라 부릅니다. 「공격자는 남의 전화번호로 번호를 끝없이 넣어 본다」가 재설정 기능의 오용 사례입니다.
보안 점검을 출시 직전에만 하는 팀에서도 보호 장치가 자주 빠집니다. 그때는 흐름이 이미 굳어 있습니다. 점검에서 나온 지적 가운데 코드 몇 줄로 메울 수 있는 것만 고치기 쉽습니다. 흐름을 바꿔야 하는 지적은 다음으로 밀립니다.
뒤늦게 고치기 어려운 까닭
구현 버그는 대개 그 버그가 있는 코드를 고치면 끝납니다. 설계 결함은 빠진 단계를 흐름에 새로 넣어야 해서 손댈 곳이 늘어납니다.
재설정 기능에 시도 횟수 제한을 넣는 경우를 보겠습니다. 틀린 횟수를 저장할 데이터가 새로 생깁니다. 막힌 사용자에게 보일 화면이 생깁니다. 고객 상담 쪽에는 막힌 사용자를 다시 시도하게 해 주는 절차가 생깁니다.
이렇게 바뀌는 곳이 화면, 서버, 데이터, 운영 절차에 걸쳐 흩어집니다. 이미 그 흐름에 맞춰 짠 코드와 이미 그 흐름에 익숙한 사용자도 있습니다. 설계 단계에서 한 줄 적었으면 됐을 일이 출시 뒤에는 여러 팀의 일이 됩니다.
이웃한 취약점과 가르는 선
안전하지 않은 설계는 무엇을 고쳐야 없어지느냐로 이웃과 갈립니다. 아래 표는 자주 견주는 넷을 나란히 놓습니다.
| 이름 | 약한 곳 | 고치는 곳 |
|---|---|---|
| 안전하지 않은 설계 | 처음부터 빠진 보호 장치 | 설계 |
| 구현 버그 | 코드를 짤 때 생긴 실수. 사용자 입력을 SQL(Structured Query Language, 데이터베이스 질의 언어) 명령에 그대로 섞는 SQL 인젝션이 대표입니다 | 코드 |
| 보안 설정 오류 | 안전하지 않은 쪽에 놓인 설정 값 | 설정 파일, 관리 화면 |
| 취약한 의존성 | 알려진 흠이 있는 옛 판 라이브러리 | 라이브러리 판 |
같은 증상이 두 갈래에서 나올 수 있습니다. 설계 문서에 시도 횟수 제한이 있었다면 이야기가 달라집니다. 개발자가 그것을 빠뜨린 것이니 구현 버그입니다. 설계 문서에 없어서 아무도 안 짰다면 안전하지 않은 설계입니다.
어느 쪽인지는 아래 순서로 물으면 가려집니다. 위에서부터 묻습니다. 답이 나오면 거기서 멈춥니다.
flowchart TD
A["이 공격을 막는 보호 장치가 설계에 있나"] -->|없다| D["안전하지 않은 설계"]
A -->|있다| B["그 장치가 이 공격을 막을 수 있나"]
B -->|못 막는다| D
B -->|막는다| C["코드가 설계대로 짜였나"]
C -->|아니다| E["구현 버그"]
C -->|그렇다| F["설정 값이 안전한 쪽에 있나"]
F -->|아니다| G["보안 설정 오류"]
F -->|그렇다| H["다른 원인을 찾는다"]
첫 두 물음에서 걸리면 설계를 고쳐야 합니다. 셋째에서 걸리면 코드를, 넷째에서 걸리면 설정을 고칩니다. 넷을 다 통과하면 낡은 라이브러리 판 같은 다른 원인을 찾습니다. 표의 취약한 의존성이 그런 원인입니다. 쿠키에 횟수를 적은 경우는 둘째 물음에서 걸리므로 설계의 문제입니다.
막는 방법
막는 방법은 대부분 코드를 짜기 전 단계에 있습니다. 이름과 하는 일만 적습니다. 자세한 것은 각 항목이 다룹니다.
| 방법 | 하는 일 |
|---|---|
| 위협 모델링 | 설계를 놓고 무엇이 어떻게 공격당할 수 있는지 찾아 막을 장치를 정합니다 |
| 오용 사례 | 기능마다 악용하는 흐름을 적어 설계 문서에 막을 방법을 함께 둡니다 |
| 보안 요구사항 | 거부해야 할 일을 요구사항에 적어 설계와 테스트가 같은 목록을 보게 합니다 |
| 속도 제한 | 같은 사용자나 같은 대상에게 짧은 시간에 받아 줄 요청 수를 정합니다. 앞에서 본 시도 횟수 제한과 같은 갈래의 장치입니다 |
| 최소 권한 | 사용자와 프로그램에 꼭 필요한 권한만 주도록 흐름을 짭니다 |
앞의 셋은 설계 문서에 빈칸이 남지 않게 하는 방법입니다. 뒤의 둘은 그 빈칸을 메울 때 자주 쓰는 보호 장치입니다.
관련 항목
안전하지 않은 설계가 속하는 상위 분류
OWASP Top 10 · OWASP · 취약점 · 보안 엔지니어링 · 애플리케이션 보안 · 웹 보안
안전하지 않은 설계를 설계 단계에서 찾는 방법
위협 모델링 · 오용 사례 · 보안 요구사항 · 보안 설계 검토 · 시큐어 바이 디자인 · STRIDE · 공격 트리
설계에 넣어야 할 보호 장치
보안 통제 · 속도 제한 · 계정 잠금 · 인증 · 인가 · 접근 제어 · 입력 검증 · 다중 인증
안전한 설계가 따르는 원칙
최소 권한 · 심층 방어 · 안전한 기본값 · 신뢰 경계 · 공격 표면 · 제로 트러스트
설계 결함을 파고드는 공격
무차별 대입 공격 · 크리덴셜 스터핑 · 계정 탈취 · 비즈니스 로직 취약점 · 봇 공격
안전하지 않은 설계와 원인이 갈리는 취약점
구현 버그 · 보안 설정 오류 · SQL 인젝션 · 취약한 의존성 · 취약한 접근 제어 · 권한 상승
설계 결함을 줄이는 개발 과정
보안 개발 수명 주기 · 시프트 레프트 · 유스케이스 · 요구사항 · 소프트웨어 설계 · 코드 리뷰
다른 이름: insecure design · 불안전한 설계 · 보안 설계 결함