TT Lab
블로그 시작하기
배우기 러닝패스 코스

비밀번호·키·비밀값 다루기

토큰은 잠깐 동안의 비밀번호다

TT Lab 에서 이어서 보기

한 줄 요약

재설정 토큰은 잠깐 동안 비밀번호 노릇을 하는 값입니다. secrets 로 충분히 길게 만들고, 서버에는 그 해시만 저장하고, 15분 안에 한 번만 쓰게 합니다. 가입 여부와 상관없이 같은 응답을 돌려줍니다.

왜 이 요구가 필요한가

'비밀번호를 잊으셨나요' 는 로그인보다 약한 문이 되기 쉽습니다. 토큰을 가진 사람은 비밀번호를 몰라도 계정을 가져가기 때문입니다. 흔한 결함을 하나씩 짚어 봅니다.

여섯 자리 숫자 토큰은 가짓수가 100만뿐입니다. 시도 횟수 제한이 느슨하면 맞힐 수 있고, random 모듈로 만들었다면 더 쉽습니다. 파이썬 문서는 random 이 모델링과 시뮬레이션을 위해 설계됐고 보안이나 암호에는 맞지 않으며, 대신 secrets 를 쓰라고 적습니다. 같은 문서는 2015년 기준으로 32바이트(256비트)의 무작위면 일반적인 쓰임에 충분하다고 봅니다.

토큰을 그대로 저장하면, DB 가 새는 순간 진행 중인 모든 재설정이 함께 샙니다. 토큰은 메일로 나가고, 링크에 실려 브라우저 기록과 프록시 로그를 지나가므로 오래 살게 두면 안 됩니다. 한 번 쓴 토큰을 다시 쓸 수 있으면, 받은편지함을 나중에 들여다본 누군가가 같은 링크로 비밀번호를 다시 바꿀 수 있습니다.

마지막으로 응답입니다. 가입한 이메일에는 '메일을 보냈습니다', 아닌 이메일에는 '그런 계정이 없습니다' 라고 답하면 이 화면이 가입 여부 조회기가 됩니다. OWASP Forgot Password Cheat Sheet 는 있는 계정과 없는 계정에 같은 메시지를 돌려주라고 적습니다.

어떻게 지키나

token = secrets.token_urlsafe(32)                      # 32바이트 무작위, URL 에 그대로 쓸 수 있다
key = hashlib.sha256(token.encode()).hexdigest()       # 표에는 해시만
tokens[key] = {"email": email, "expires": clock() + 15 * 60, "used": False}

토큰에는 느린 해시가 필요 없습니다. 비밀번호는 사람이 고른 값이라 경우의 수가 적어서 느린 해시로 맞히기를 늦춰야 하지만, 32바이트 무작위 토큰은 SHA-256 한 번이면 되돌릴 수 없습니다. 이것이 비밀번호와 토큰의 다른 점입니다.

쓸 때는 받은 토큰을 같은 방법으로 해시해 표에서 찾고, 만료됐거나 이미 쓴 토큰은 거절합니다. 비밀번호를 바꾸기 전에 쓴 것으로 표시해야 같은 토큰을 동시에 두 번 내는 요청에도 한 번만 성공합니다. 같은 이메일로 새 토큰을 만들면 옛 토큰은 지웁니다. 가장 최근 메일의 링크만 살아 있어야 사용자가 무엇이 유효한지 헷갈리지 않습니다.

응답이 같아야 하는 것은 본문만이 아닙니다. 가입한 이메일일 때만 메일을 보내느라 응답이 눈에 띄게 늦어지면 시간으로 가입 여부가 드러납니다. 메일 발송은 대기열에 넘기고 바로 답하는 구조가 그래서 흔합니다. 이 실습의 서비스는 메일을 목록(outbox)에 쌓기만 하므로 시간 차이가 거의 없지만, 운영 코드를 볼 때는 이 점도 함께 봅니다.

시계는 바꿔 끼울 수 있게 둡니다. clock=lambda: now[0] 처럼 넘기면 시험에서 15분을 기다리지 않고 now[0] += 901 로 보낼 수 있습니다. 시간에 기대는 보안 규칙은 시계를 주입할 수 있어야 시험으로 고정됩니다.

현장에서 만나는 모습

재설정 링크를 연 화면이 바깥 분석 스크립트를 불러오면, 토큰이 든 주소가 Referer 로 바깥에 나갈 수 있습니다. 그래서 재설정 화면은 외부 자원을 줄이고 Referrer-Policy 를 엄격히 둡니다. 재설정에 성공하면 그 계정의 다른 세션을 끊는 것도 흔한 요구인데, 세션과 쿠키 이야기는 '세션과 토큰' 코스가 다룹니다.

시도 횟수 제한도 함께 둡니다. 토큰이 충분히 길면 맞히기는 사실상 불가능하지만, 재설정 요청 자체를 쏟아 내 메일함을 채우거나 메일 발송 비용을 키우는 일은 막아야 합니다. 이메일당, 그리고 요청한 곳당 시간 창을 두고 요청 수를 세는 방식은 'FastAPI 로 서비스 만들기' 코스의 요청 수 제한 실습과 같습니다.

감사 로그에는 토큰을 남기지 않습니다. 대신 토큰 해시의 앞 8글자(token_id)를 남기면 요청과 사용을 이어 볼 수 있고, 그 값으로 토큰을 되살릴 수는 없습니다. 거절 이유(없음, 만료, 이미 씀)는 남겨 두어야 '링크가 안 돼요' 라는 문의에 답할 수 있습니다.

English corner

다음 실습에서 할 것

재설정 서비스가 여섯 자리 숫자를 random 으로 만들어 그대로 저장하고, 만료도 한 번 쓰기도 없으며, 없는 이메일에는 404 를 줍니다. 요구 A(무작위·해시 저장·같은 응답)와 요구 B(15분·한 번·새 토큰이 옛 토큰을 지움)를 바꿔 끼운 시계로 시험하고 고칩니다.