[주요정보통신기반시설 웹 애플리케이션 취약점 분석·평가 21일 완성 코스] 12강 — [Day 12] 취약한 비밀번호 복구 절차

⚡ 정다은 · 2026-10-02 14:00 · 조회 45

안녕하세요, 해커스토리 수강생 여러분! IT/보안 튜터 정다은입니다. 👋

어느덧 21일 완성 코스의 반환점을 돌아 12일 차 강의에 도달했습니다! 지난 시간까지 학습한 강력한 인증 정책과 권한 검증 수칙들을 잘 다져두셨나요?

오늘 다룰 주제는 보안 시스템의 강력한 '정문'을 잘 만들어두고도, 정작 '뒷문'을 뚫리게 만드는 치명적인 허점, 바로 [Day 12] 취약한 비밀번호 복구 절차입니다.

서비스를 운영하다 보면 비밀번호를 잊어버린 사용자를 위해 **"비밀번호 찾기"**나 "비밀번호 재설정" 기능을 반드시 제공해야 합니다. 하지만 이 복구 프로세스는 본인 인증을 '우회'하여 계정의 권한을 재설정하는 작업이기 때문에, 공격자에게는 정당한 로그인 절차보다 훨씬 탐나는 공격 표적이 됩니다.

실무 웹 진단 현장에서 자주 발견되는 이 취약점의 원리와 실제 공격 사례, 그리고 안전한 방어 코드 작성법까지 차근차근 알아보겠습니다!


1. 취약한 비밀번호 복구 절차란? (개념 설명)

비밀번호 복구 절차 취약점은 사용자의 신원을 재확인하고 비밀번호를 변경해 주는 과정에서 인증 논리나 암호화 메커니즘이 불충분하여, 공격자가 타인의 계정을 무단으로 탈취(Account Takeover)할 수 있는 보안 결함을 의미합니다.

개발자들은 로그인 화면에는 강력한 암호화와 brute-force 방지 로직을 구현하지만, 비밀번호 재설정 페이지는 비교적 가볍게 구현하는 실수를 자주 범합니다. 대표적인 취약점 패턴은 다음과 같이 4가지로 나뉩니다.

       [ 취약한 비밀번호 복구 절차의 주요 유형 ]

 1. 추측하기 쉬운 본인 확인 질문 (보안 질문)
    └─ "출신 초등학교는?", "보물 1호는?" -> SNS/구글링으로 타겟 정보 수집 가능

 2. 예측 가능한 재설정 토큰 / 임시 비밀번호
    └─ 타임스탬프, 순차적 ID, 사용자 ID를 MD5/Base64로 단순 변환하여 사용

 3. 토큰 관리 부실 (만료 시간 미설정 & 재사용 가능)
    └─ 한 번 발급된 재설정 링크가 평생 유효하거나, 여러 번 재사용 가능

 4. 사용자 존재 여부 노출 (User Enumeration)
    └─ "존재하지 않는 이메일입니다" vs "인증 메일을 발급했습니다" 메시지 차이

2. 실무 사례 및 코드 예시 (동작 원리 분석)

❌ 취약한 예시: 예측 가능한 재설정 토큰 생성

과거에 실제로 자주 발견되었던 취약한 백엔드 로직을 살펴봅시다.

# [취약한 코드 예시 - Python/Flask]
import hashlib
import time

def generate_reset_token(username):
    # 취약점: 토큰 생성이 '현재 시간(Timestamp)'과 '사용자 ID'에만 의존함!
    current_time = str(int(time.time()))
    raw_string = username + current_time
    
    # MD5는 단순 해시 알고리즘이며 난수성이 전혀 없음
    token = hashlib.md5(raw_string.encode()).hexdigest()
    return token

💣 공격 시나리오

  1. 공격자는 타겟 사용자(victim@example.com)의 비밀번호 재설정을 요청합니다.
  2. 공격자는 요청이 전송된 **정확한 시각(Unix Timestamp)**을 알고 있습니다. (예: 1700000000)
  3. 공격자는 내 컴퓨터에서 victim@example.com + 1700000000 (및 앞뒤 1~2초 오차 range)를 조합하여 MD5 해시를 직접 생성합니다.
  4. 공격자는 피해자의 이메일을 확인하지 않고도, 직접 계산해 낸 토큰값으로 https://target.com/reset-password?token=계산된_MD5_토큰 URL에 접속하여 피해자의 비밀번호를 자신의 마음대로 변경해 버립니다.

❌ 취약한 예시: 추측하기 쉬운 보안 질문 (Security Questions)

과거 시스템에서 자주 쓰이던 "보물 1호는?", "출신 초등학교는?" 같은 질문 방식입니다.

  • 공격 방법: 공격자는 피해자의 페이스북, 인스타그램 등 SNS를 탐색하여 피해자의 출신 초등학교나 반려견 이름을 쉽게 알아냅니다.
  • 결과: 별도의 인증 토큰이나 이메일 인증 없이 단순 브루트포스(Brute-force)나 사전 공격(Dictionary Attack)만으로 계정이 탈취됩니다.

3. 안전한 비밀번호 복구 프로세스 설계 및 구현

그렇다면 안전한 비밀번호 재설정 기능은 어떻게 구현해야 할까요? 핵심 원칙 4가지를 기억하세요!

  1. 보안 질문(Security Questions) 제거: 더 이상 보안 질문 방식은 사용하지 않고, 이메일/SMS OTP/소셜 인증 등 **대역 외 인증(Out-Of-Band Authentication)**을 사용합니다.
  2. CSPRNG 기반의 예측 불가능한 난수 토큰 생성: 단순 Random이나 Timestamp 기반이 아닌, 암호학적으로 안전한 난수 생성기(CSPRNG)를 사용합니다.
  3. 토큰의 단방향 암호화 저장 및 유효기간 설정: DB에는 토큰의 해시값만 저장하고, 유효기간(예: 10~15분)과 1회용 사용 처리(Single-Use)를 적용합니다.
  4. 계정 존재 여부 힌트 차단: 이메일 입력 시 가입 여부를 외부에서 눈치채지 못하도록 동일한 안내 메시지를 출력합니다.

✅ 안전한 코드 예시 (Python / Flask)

Below is a secure implementation demonstrating token generation using Python's secrets module, SHA-256 token hashing for database storage, and proper expiration logic.

import secrets
import hashlib
from datetime import datetime, timedelta

# 1. 암호학적으로 안전한 무작위 토큰 생성 (CSPRNG)
def generate_secure_reset_token():
    # 32바이트(256비트)의 예측 불가능한 URL-Safe 난수 토큰 생성
    raw_token = secrets.token_urlsafe(32)
    
    # DB 저장용 SHA-256 해시 생성 (DB가 털려도 원본 토큰 유출 방지)
    token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
    
    # 토큰 유효기간 설정 (10분)
    expiration_time = datetime.utcnow() + timedelta(minutes=10)
    
    return raw_token, token_hash, expiration_time

# 2. 비밀번호 재설정 요청 처리 함수
def request_password_reset(user_email):
    user = find_user_by_email(user_email)
    
    # [보안 조치] 계정이 존재하지 않아도 공격자가 알 수 없도록 동일 응답 처리 (User Enumeration 방지)
    if not user:
        return "입력하신 이메일로 재설정 링크가 전송되었습니다. (가입되어 있는 경우)"
    
    raw_token, token_hash, expiration = generate_secure_reset_token()
    
    # DB에 원본 토큰이 아닌 해시값과 만료시간, 사용 여부(is_used)를 저장
    save_reset_token_to_db(
        user_id=user.id, 
        token_hash=token_hash, 
        expires_at=expiration, 
        is_used=False
    )
    
    # 사용자 이메일로는 'raw_token'이 담긴 링크 발송
    reset_link = f"https://example.com/reset-password?token={raw_token}"
    send_reset_email(user_email, reset_link)
    
    return "입력하신 이메일로 재설정 링크가 전송되었습니다."

# 3. 토큰 검증 및 비밀번호 변경 처리 함수
def verify_and_reset_password(raw_token, new_password):
    # 전달받은 raw_token을 동일한 방식으로 해시화하여 DB 조회
    incoming_hash = hashlib.sha256(raw_token.encode()).hexdigest()
    token_record = find_token_in_db(incoming_hash)
    
    # 토큰 존재 여부, 만료 여부, 이미 사용되었는지 검증
    if not token_record or token_record.is_used:
        return "유효하지 않거나 이미 사용된 토큰입니다.", 400
        
    if datetime.utcnow() > token_record.expires_at:
        return "만료된 토큰입니다. 재설정을 다시 요청해 주세요.", 400
        
    # 비밀번호 변경 및 토큰 폐기 처리 (1회용 보장)
    update_user_password(token_record.user_id, hash_password(new_password))
    mark_token_as_used(token_record.id) # 사용 완료 처리!
    
    return "비밀번호가 성공적으로 변경되었습니다.", 200

4. 핵심 내용 요약 체크리스트

실무 진단 및 보안 개발 시 아래 체크리스트를 점검하세요!

점검 항목 안전한 설정 기준
토큰 난수성 secrets, SecureRandom 등 CSPRNG를 통한 최소 128비트 이상의 무작위 문자열인가?
토큰 유효기간 재설정 토큰의 유효기간이 10~15분 이내로 짧게 설정되어 있는가?
1회성 보장 토큰 사용 즉시 무효화(Single-use)되며, 이전 토큰은 새로 요청 시 즉시 파기되는가?
DB 저장 방식 DB에 토큰을 저장할 때 원본이 아닌 **단방향 해시(SHA-256 등)**로 저장하는가?
정보 노출 방지 재설정 요청 시 등록되지 않은 이메일/ID 입력에 대해 동일한 메시지를 반환하는가?
시도 횟수 제한 OTP/PIN(6자리 숫자인 경우) 입력 시 브루트포스를 막기 위한 **Rate Limiting(예: 5회 제한)**이 존재하는가?

🎓 강의를 마치며

오늘은 웹 애플리케이션의 대표적인 우회 공격 경로인 취약한 비밀번호 복구 절차에 대해 알아보았습니다. 아무리 강력한 비밀번호 규칙(Day 9)과 세션 관리 정책을 세워두어도, 복구 로직이 허술하면 전체 서비스의 보안이 일순간에 무너질 수 있다는 점을 꼭 기억해 주세요!

다음 시간에는 [Day 13] 프로세스 검증 누락에 대해 공부합니다. 웹 애플리케이션이 다단계(Multi-step) 프로세스(예: 결제, 회원가입 단계, 본인 인증 단계)를 처리할 때, 사용자가 중간 과정을 강제로 건너뛰거나 순서를 조작할 수 있는 비즈니스 로직 취약점을 집중적으로 다룰 예정입니다.

궁금한 점이 있다면 언제든 댓글로 질문 남겨주세요. 수고 많으셨습니다! 다음 강의에서 만나요! 🚀

댓글 0

로그인하면 댓글을 작성할 수 있습니다.