설명
MD5와 SHA-1은 충돌 저항성이 필요한 보안 용도에 적합하지 않습니다. 비밀번호 저장에는 이들처럼 빠른 해시 대신 솔트와 조절 가능한 작업량을 갖춘 전용 비밀번호 해시가 필요합니다. 한편 키 없는 해시로 다운로드 링크나 토큰을 인증하면 공격자가 값을 바꾸고 해시를 다시 계산할 수 있으며, 이는 충돌을 만들지 않아도 가능한 위조입니다.
잠재적 영향
- 인증 우회/위조: 검증 방식에 따라 MD5·SHA-1 충돌을 이용해 약한 해시에 의존하는 무결성 검사를 우회할 수 있습니다.
- 비밀번호 탈취(브루트포스/레인보우 테이블): 빠른 해시와 솔트 미사용 시 대량 대입이 쉬워 비밀번호가 빠르게 노출될 수 있습니다.
- 데이터 무결성 약화: 변경 감지가 실패해 위·변조된 데이터가 정상으로 처리될 수 있습니다.
- 토큰/링크 서명 위조: 키 없는 단순 해시로 보호된 토큰·다운로드 링크 등은 위조될 수 있습니다.
해결 방법
- 비밀번호 저장
- Argon2id 권장(또는 bcrypt, scrypt, PBKDF2-HMAC-SHA256). 충분한 작업량(Iterations)·메모리 비용 설정, 랜덤 Salt 필수, 비교는 constant-time(hmac.compare_digest) 사용.
- PBKDF2-HMAC-SHA256을 사용할 때는 600,000회 이상과 16바이트 이상의 무작위 솔트를 적용하고, 실제 서비스의 처리 성능에 맞게 비용을 조정합니다.
- 무결성/인증 목적
- 진위성 확인이 필요하면 HMAC-SHA256 같은 키 기반 MAC을 사용합니다.
- 단순 체크섬 용도 외에는 MD5·SHA-1 사용 중단. 필요 시 SHA-256/512 또는 SHA-3 계열 사용.
- 마이그레이션 전략
- 로그인 시 재해싱(rehash)로 단계적 교체, 저장 포맷에 알고리즘/파라미터를 포함해 점진적 업그레이드 가능하게 설계.
예시
변경 전
python
import hashlib
# 민감 데이터(비밀번호, 토큰)에 MD5/SHA-1 사용 - 취약
def store_password_md5(username: str, password: str) -> str:
# BAD: 빠르고 충돌 취약한 MD5로 비밀번호 해싱, 솔트 없음
digest = hashlib.md5(password.encode("utf-8")).hexdigest()
return f"{username}:{digest}"
def sign_download_link_sha1(user_id: str, expires: int) -> str:
# BAD: 키 없이 단순 SHA-1 해시로 서명 비슷하게 사용 -> 위조 가능
data = f"{user_id}:{expires}".encode()
sig = hashlib.sha1(data).hexdigest()
return f"{user_id}:{expires}:{sig}"
변경 후
python
import os
import hmac
import base64
import hashlib
import secrets
# 안전한 비밀번호 저장: PBKDF2-HMAC-SHA256 사용
def hash_password(password: str) -> str:
salt = os.urandom(16)
iterations = 600_000
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations)
return f"pbkdf2_sha256${iterations}${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}"
def verify_password(password: str, stored: str) -> bool:
scheme, iters_s, b64_salt, b64_dk = stored.split("$")
if scheme != "pbkdf2_sha256":
return False
salt = base64.b64decode(b64_salt)
iterations = int(iters_s)
true_dk = base64.b64decode(b64_dk)
test_dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations)
return hmac.compare_digest(true_dk, test_dk)
# 안전한 무결성/인증: HMAC-SHA256 사용(비밀키 필요)
def sign_download_link(user_id: str, expires: int, secret_key: bytes) -> str:
# secret_key는 secrets.token_bytes(32) 등으로 생성해 안전 저장
msg = f"{user_id}:{expires}".encode()
mac = hmac.new(secret_key, msg, hashlib.sha256).hexdigest()
return f"{user_id}:{expires}:{mac}"
설명:
- 변경 전:
- MD5/SHA-1은 충돌에 취약하고 매우 빨라 비밀번호 크래킹에 유리합니다. 솔트가 없으면 레인보우 테이블 공격까지 가능해집니다.
- 키 없는 SHA-1 해시로 링크나 토큰을 보호하면 공격자가 내용을 변경하고 해시도 다시 계산할 수 있습니다.
- 변경 후:
- 비밀번호는 PBKDF2-HMAC-SHA256으로 느리게(600,000회) 계산하고, 랜덤 Salt를 사용해 브루트포스·레인보우 테이블 공격을 어렵게 만듭니다. 비교는 hmac.compare_digest로 시간 기반 사이드채널을 줄입니다.
- 무결성/인증은 HMAC-SHA256처럼 키 기반 MAC을 사용해 단순 해시 위조를 방지합니다. 강한 알고리즘과 안전한 키 관리로 충돌/위조 위험을 완화합니다.