비밀번호 대상으로 취약한 일반 해시 함수 사용

Use of Password Hash with Insufficient Computational Effort

설명

비밀번호에 SHA-2/3, BLAKE2 같은 일반 해시를 직접 적용하면 연산 비용이 낮아 GPU/ASIC로 매우 빠르게 크래킹할 수 있습니다. 공격자가 유출된 해시를 입수하면 사전 목록이나 무차별 대입으로 많은 후보를 시험할 수 있으며, 약하거나 재사용된 비밀번호는 특히 위험합니다. 이 취약점은 해시가 유출되거나 백업·로그에 노출될 때 오프라인 크래킹이 특히 쉬워져 계정 탈취로 이어질 수 있습니다.

잠재적 영향

  • 계정 탈취: 빠른 오프라인 크래킹으로 원문 비밀번호를 복구해 계정에 로그인할 수 있습니다.
  • 크리덴셜 재사용에 따른 연쇄 침해: 한 서비스에서 털린 비밀번호로 다른 서비스까지 무단 접속이 가능해집니다.
  • 데이터 유출 확대: 관리자 계정 탈취 시 더 넓은 범위의 시스템/데이터에 접근 가능합니다.
  • 보안 요구사항 미충족: 조직에 적용되는 비밀번호 저장 요구사항을 충족하지 못하면 보안 감사에서 지적될 수 있습니다.

해결 방법

  • 비밀번호 전용 해시 사용: Argon2id, scrypt, bcrypt, PBKDF2처럼 계산 비용을 조절할 수 있는 전용 방식을 사용하세요.
  • 강한 파라미터 설정:
    • Argon2id: 아래 예시는 time_cost=3, 메모리 64 MiB, parallelism=2를 사용합니다. 운영 환경에서 벤치마크하여 조정하세요.
    • PBKDF2-HMAC-SHA256: 최소 600,000회 반복을 사용하고 서버 성능에 맞춰 비용을 조정하세요.
    • bcrypt: cost(work factor) 12 이상 권장
    • scrypt: N/r/p를 서버 성능에 맞춰 충분히 크게 설정
  • 솔트(salt): 사용자마다 고유한 랜덤 salt(≥16바이트)를 생성해 저장하십시오. 재사용 금지.
  • 페퍼(pepper): 사용하는 경우 데이터베이스와 분리된 보호 저장소에서 비밀 키를 관리하고 검증된 결합 방식을 적용하세요.
  • 안전한 비교: 비밀번호 해시 라이브러리의 검증 API를 사용하세요. 직접 다이제스트를 비교해야 한다면 hmac.compare_digest 같은 일정 시간 비교를 사용하세요.
  • 비밀번호 정책: 충분한 길이, 입력 최대 길이 제한, 요청 속도 제한 또는 계정 잠금으로 온라인 공격도 완화하세요.
  • 검증된 라이브러리 사용: argon2-cffi, passlib 같은 전용 라이브러리를 사용하고 자체 구현은 피하세요.

예시

변경 전

python
import hashlib

# 빠른 일반 해시(sha3_256)와 정적 salt를 사용하는 잘못된 예시
STATIC_SALT = b"NaCl-2024"  # 사용자별 고유 salt가 아님

def insecure_hash_password(password: str) -> str:
    data = password.encode("utf-8") + STATIC_SALT
    digest = hashlib.sha3_256(data).hexdigest()
    return digest

# 검증도 동일한 방식으로 재계산 후 문자열 비교(타이밍 누출 가능)
def insecure_verify_password(password: str, stored_hash: str) -> bool:
    calc = insecure_hash_password(password)
    return calc == stored_hash

변경 후

python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

# Argon2id 사용: 메모리-하드, 느리고 GPU에 불리함
ph = PasswordHasher(time_cost=3, memory_cost=64 * 1024, parallelism=2, hash_len=32)

def secure_hash_password(password: str) -> str:
    # 내부적으로 랜덤 salt를 포함한 포맷으로 해시를 생성해 줍니다.
    return ph.hash(password)

def secure_verify_password(password: str, stored_hash: str) -> bool:
    try:
        return ph.verify(stored_hash, password)
    except VerifyMismatchError:
        return False

설명:

  • 변경 전:
    • sha3_256 같은 빠른 일반 해시는 계산 비용이 낮아 GPU/ASIC로 매우 빠르게 brute-force가 가능합니다.
    • 정적 salt를 사용해 동일 비밀번호의 해시가 사용자 간 동일해지며 rainbow table 대응이 약화됩니다.
    • 문자열 비교는 constant-time이 아니어서 타이밍 기반 정보 누출 위험이 있습니다.
  • 변경 후:
    • Argon2id는 메모리-하드 특성으로 대규모 병렬 크래킹 비용을 크게 증가시킵니다.
    • PasswordHasher가 각 해시에 새로운 랜덤 salt를 자동 포함합니다.
    • verify를 사용해 안전하게 검증하며, 파라미터는 서버 성능을 기준으로 벤치마크 후 상향 조정 가능합니다.

참조