![[실전 보안 03] 비밀번호를 그대로 저장하면 안 된다](https://img.thenullpage.com/posts/5710/5710_1_13aa4d.webp)
지난 편에서 인증의 증표 이야기를 하며, 로그인 관문 자체가 안전해야 그 뒤가 의미 있다고 했다. 이번 편은 그 관문의 열쇠, 비밀번호를 어떻게 저장해야 하는지에 대한 이야기다. 처음 회원가입 기능을 만들 때 나도 무심코 사용자가 입력한 비밀번호를 그대로 데이터베이스에 넣을 뻔했다. 다행히 그 직전에 관련 자료를 읽고 등골이 서늘해졌다. 비밀번호를 있는 그대로 저장하는 건 사용자에게 저지를 수 있는 가장 위험한 실수 중 하나였다. 왜 그런지, 그리고 어떻게 해야 하는지를 정리해 본다.
1. 평문 저장이 부르는 재앙
비밀번호를 입력받은 그대로, 그러니까 평문으로 저장한다고 해 보자. 평소에는 아무 문제가 없어 보인다. 로그인할 때 입력값과 저장값을 비교해 같으면 통과시키면 되니 구현도 간단하다. 문제는 그 저장소가 언젠가 유출될 수 있다는 데 있다. 데이터베이스는 여러 이유로 새어 나간다. 설정 실수, 취약점, 내부자, 백업 파일 방치 등 경로는 무수히 많다.
평문으로 저장했다면 유출되는 순간 모든 사용자의 비밀번호가 그대로 공격자 손에 들어간다. 더 심각한 건 이차 피해다. 사람들은 여러 사이트에 같은 비밀번호를 돌려쓴다. 우리 사이트에서 샌 비밀번호로 공격자는 그 사람의 이메일, 다른 커뮤니티, 심하면 금융 서비스까지 시도해 본다. 지난 편에서 말한 유출 목록 대입 공격의 재료가 바로 이렇게 만들어진다. 내 사이트의 부주의가 사용자의 삶 전체를 위협하는 셈이다.
그래서 원칙은 단호하다. 서버는 사용자의 비밀번호를 절대 알아서는 안 된다. 운영자인 나조차 특정 사용자의 비밀번호가 무엇인지 알 수 없어야 한다. 사용자가 비밀번호를 잊었을 때 원래 비밀번호를 알려 주는 게 아니라 새로 설정하게 하는 이유가 여기 있다. 원래 값을 알려 줄 수 있다는 건 그 사이트가 비밀번호를 복원 가능한 형태로 갖고 있다는 뜻이고, 그건 위험 신호다.
2. 해싱, 되돌릴 수 없게 만들기
그럼 저장은 안 하는데 어떻게 로그인을 확인할까. 답은 해싱이다. 해싱은 입력값을 일정한 길이의 알아볼 수 없는 문자열로 바꾸는 계산인데, 핵심 성질이 두 가지다. 첫째, 같은 입력은 항상 같은 결과를 낸다. 둘째, 결과에서 원래 입력을 거꾸로 계산해 내는 게 사실상 불가능하다. 한 방향으로만 흐르는 계산이라 단방향 함수라고도 부른다.
이 성질을 이용하면 비밀번호 자체를 저장하지 않고도 검증할 수 있다. 회원가입 때 비밀번호를 해싱해 그 결과만 저장하고 원래 값은 버린다. 로그인 때 입력받은 비밀번호를 똑같이 해싱해, 저장된 결과와 비교한다. 같으면 통과다. 서버는 원래 비밀번호를 끝내 모르지만 검증은 할 수 있다. 유출되더라도 공격자가 손에 넣는 건 되돌릴 수 없는 해시값뿐이다.
다만 해싱이면 무조건 안전한 건 아니었다. 일반적인 빠른 해시 함수는 원래 데이터 무결성 검사 같은 용도로 만들어져 계산이 매우 빠르다. 빠르다는 건 공격자가 초당 수억, 수십억 번씩 후보 비밀번호를 해싱해 맞춰 볼 수 있다는 뜻이기도 하다. 흔한 비밀번호라면 이런 무차별 대입에 금방 뚫린다. 그래서 비밀번호에는 일부러 느리게, 그리고 특별한 방식으로 설계된 전용 해시를 써야 한다.
3. 소금과 느림, 두 가지 방어
비밀번호 해싱을 제대로 하려면 두 가지 개념이 필수였다. 첫째는 소금이라 부르는 무작위 값이다. 사용자마다 서로 다른 무작위 값을 비밀번호에 섞은 뒤 해싱한다. 이렇게 하면 같은 비밀번호를 쓰는 두 사람도 저장된 해시값이 완전히 달라진다. 소금이 왜 중요하냐면, 공격자들이 흔한 비밀번호의 해시값을 미리 계산해 둔 거대한 대조표를 갖고 있기 때문이다.
이 대조표를 흔히 무지개 표라고 부른다. 소금이 없으면 유출된 해시값을 이 표와 대조하는 것만으로 원래 비밀번호가 순식간에 드러난다. 하지만 사용자마다 다른 소금이 섞여 있으면 미리 만든 표가 무력해진다. 공격자는 사용자 한 명 한 명을 따로 공략해야 하고, 그만큼 비용이 폭증한다. 소금은 저장할 때 해시값과 함께 보관하며, 비밀로 감출 필요는 없다. 대량 공격을 개별 공격으로 쪼개는 게 목적이기 때문이다.
둘째는 의도된 느림이다. 비밀번호 전용 해시는 계산에 상당한 시간과 메모리를 일부러 소모하도록 설계돼 있다. 정상 로그인은 한 번에 한 번만 계산하니 약간 느린 게 전혀 문제가 안 된다. 하지만 수십억 번을 시도해야 하는 공격자에게는 이 작은 지연이 곱해져 감당 못 할 시간이 된다. 얼마나 느리게 할지는 설정으로 조절할 수 있어, 하드웨어가 빨라질수록 강도를 높여 대응한다.
4. 어떤 알고리즘을 쓸 것인가
구체적으로 어떤 해시를 골라야 하는지 찾아보니 업계 권고가 비교적 명확했다. 현재 가장 권장되는 건 메모리를 많이 쓰도록 설계된 최신 알고리즘 계열이다. 이런 알고리즘은 단순히 느릴 뿐 아니라 계산에 상당한 메모리를 요구한다. 공격자들이 흔히 쓰는 대규모 병렬 연산 장치는 계산은 빨라도 메모리가 부족한 경우가 많아, 메모리를 많이 요구하는 방식이 특히 효과적인 방어가 된다.
메모리 하드 방식을 쓰기 어려운 환경이라면 오래 검증된 전통적인 비밀번호 해시도 여전히 유효한 선택이다. 이 경우 작업 강도를 나타내는 설정값을 충분히 높게 잡는 게 중요하다. 값이 클수록 계산이 느려지고 공격 비용이 오른다. 다만 이런 전통 방식에는 입력 길이에 제한이 있는 등 알아 둬야 할 특성이 있어, 무작정 쓰기보다 문서를 확인하고 적용해야 안전하다.
여기서 내가 얻은 가장 큰 교훈은 절대 직접 만들지 말라는 것이었다. 비밀번호 저장 로직을 스스로 발명하고 싶은 유혹이 들 수 있지만, 이 분야는 수많은 전문가가 수십 년간 다듬어 온 영역이다. 검증된 라이브러리를 그대로 쓰는 게 항상 옳았다. 소금 생성, 강도 조절, 검증 절차가 이미 안전하게 구현돼 있으니, 나는 그저 올바르게 호출하기만 하면 됐다. 코드로 치면 hash(password) 저장, verify(input, stored) 비교, 이 두 함수의 조합이 전부였다.
5. 저장 너머의 습관들
비밀번호를 안전하게 저장하는 것만으로 끝이 아니었다. 오가는 길에서 새지 않게 하는 것도 똑같이 중요했다. 로그인 정보는 반드시 암호화된 통신으로만 주고받아야 한다. 그렇지 않으면 아무리 잘 저장해도 입력하는 순간 중간에서 가로채인다. 다행히 요즘은 암호화 통신이 기본이라 이 부분은 상대적으로 신경 쓸 일이 줄었지만, 원칙 자체는 잊지 말아야 한다.
또 하나는 비밀번호 정책이다. 너무 짧거나 흔한 비밀번호를 아예 못 쓰게 막는 것만으로도 방어력이 크게 올라간다. 다만 지나치게 복잡한 규칙을 강요하면 사용자가 오히려 뻔한 변형을 쓰거나 어딘가에 적어 두게 돼 역효과가 난다. 최소 길이를 넉넉히 잡고 명백히 유출된 흔한 비밀번호만 걸러 내는 정도가 실용적이었다. 사용자가 긴 문장형 비밀번호를 편하게 쓰도록 유도하는 게 더 효과적이었다.
한 가지 더, 유출은 남의 일이 아니라는 마음가짐이 중요했다. 나는 우리 사이트가 작아서 유출될 리 없다고 여기지 않았다. 오히려 작은 사이트일수록 방어가 허술해 유출 위험이 크다는 걸 첫 편에서 이미 확인했다. 그래서 저장 방식을 정할 때 항상 최악의 경우, 그러니까 데이터베이스가 통째로 새어 나갔을 때 사용자에게 얼마나 피해가 갈지를 기준으로 삼았다. 이 기준으로 보면 평문 저장은 논외였고, 느린 전용 해시에 소금을 더하는 방식이 유일하게 마음이 놓이는 선택이었다.
이렇게 비밀번호를 안전하게 다뤄도 남는 구멍이 있다. 애초에 그 계정을 만든 사람이 진짜 사람인지, 유효한 연락처를 가진 사용자인지는 비밀번호만으로 알 수 없다. 봇이 가짜 정보로 계정을 대량 생성하는 걸 막으려면 다른 장치가 필요하다. 정리하면 비밀번호 저장의 원칙은 세 문장으로 압축된다. 평문으로 저장하지 않는다, 사용자마다 다른 소금을 섞어 느린 전용 해시로 저장한다, 그리고 그 구현은 직접 만들지 말고 검증된 라이브러리에 맡긴다. 이 세 가지만 지켜도 데이터베이스가 유출되는 최악의 상황에서 사용자를 지킬 마지막 방어선이 선다. 화려한 기술보다 이 기본기를 빠뜨리지 않는 것이 훨씬 중요하다는 걸 나는 여러 사고 사례를 읽으며 절감했다.
그래서 다음 편에서는 이메일 인증 이야기를 하려 한다. 회원가입 과정에 이메일 확인을 끼워 넣는 것이 왜 스팸 계정을 크게 줄여 주는지, 어떻게 구현했는지를 다뤄 보겠다.