본문 바로가기
문서비밀번호를 md5/sha1로 저장해요

비밀번호를 md5/sha1로 저장해요

md5·sha1은 너무 빨라서 요즘 장비로는 초당 수십억 번 대입해 비밀번호를 되찾을 수 있어요. DB가 한 번 새면 회원 비밀번호는 사실상 평문입니다.

한눈에
위험도
높음(High)
CWE
CWE-916 · 계산 비용이 부족한 해시
OWASP
A02:2021
vg-weak-password-hash

이게 무슨 말이냐 (쉽게)

해시는 "되돌릴 수 없게 섞는 것"이 아니라 "되돌리는 데 시간이 아주 오래 걸리게 만드는 것"입니다. md5·sha1은 파일이 바뀌었는지 확인하려고 만든 알고리즘이라 빠른 게 장점이에요. 비밀번호에는 그 장점이 그대로 약점이 됩니다.

bcrypt·argon2는 반대로 일부러 느리게 설계됐습니다. 로그인 한 번에 0.2초는 사람에게 티도 안 나지만, 수억 번 대입하려는 쪽에는 벽이 됩니다.

"소금(salt)을 쳤으니 괜찮다"도 아닙니다. salt는 같은 비밀번호가 같은 값이 되는 걸 막아줄 뿐, 속도를 늦추지는 못해요.

실제로 어떻게 터지나 (시나리오)

회원 5,000명짜리 서비스에서 백업 파일 노출이나 SQL 인젝션으로 users 테이블이 한 번 새어 나갑니다. 비밀번호 칸에는 5f4dcc3b5aa765d61d8327deb882cf99 같은 md5 값이 들어 있어요.

공격자는 이걸 해독하지 않습니다. 흔한 비밀번호 수억 개의 md5 값은 이미 표로 만들어져 인터넷에 돌아다니거든요. 대조는 몇 분이면 끝나고, 보통 절반 이상이 맞습니다. 위 값은 password고요.

그다음이 진짜 피해입니다. 사람들은 비밀번호를 돌려 쓰기 때문에, 공격자는 그 이메일·비밀번호 쌍을 다른 서비스에 그대로 넣어봅니다(크리덴셜 스터핑). 당신 서비스에서 샌 비밀번호로 회원의 메일함이 털려요.

최악의 경우

나한테 해당되나? (체크)

→ 2·3번이 같이 체크되면 해당됩니다. 이 룰은 휴리스틱이라 리포트에는 신뢰도 점검 필요로 떠요 — md5를 비밀번호가 아니라 캐시 키에 쓰고 있다면 오탐이에요.

어떻게 고치나 (복붙)

① 가장 게으르고 가장 안전한 답 — 직접 만들지 않기. Supabase Auth·Clerk·NextAuth 같은 인증 서비스를 쓰면 비밀번호가 당신 DB에 저장되지 않습니다. 새 프로젝트라면 이게 정답입니다.

② 직접 해야 한다면 bcrypt(또는 argon2)

import bcrypt from "bcrypt";

// 가입 — 저장할 때
const hash = await bcrypt.hash(password, 12);            // cost 12 이상
await db.users.insert({ email, password_hash: hash });

// 로그인 — 확인할 때
const ok = await bcrypt.compare(password, user.password_hash);   // 문자열 직접 비교 금지

③ 기존 회원은 "로그인할 때 자동 교체". 이미 저장된 md5 값은 되돌릴 수 없으니, 다음 로그인 때 갈아끼웁니다. 회원에게 아무것도 요구하지 않고 넘어가는 방법이에요.

if (user.password_hash.length === 32) {          // 옛 md5 값
  if (md5(password) !== user.password_hash) return fail();
  await db.users.update(user.id, {
    password_hash: await bcrypt.hash(password, 12),   // 통과한 그 자리에서 교체
  });
} else if (!(await bcrypt.compare(password, user.password_hash))) {
  return fail();
}

교체가 끝나면(또는 정해둔 기한이 지나면) 옛 경로를 삭제하세요. 남겨두면 그게 곧 뒷문입니다.

④ 이미 유출됐다면 — 전원 비밀번호 재설정과 유출 통지가 먼저입니다. 해시만 바꾸는 걸로 끝나지 않습니다.

고친 뒤 확인

  1. 01

    값 모양 확인

    bcrypt는 $2b$12$...로 시작하는 60자입니다. 32자 16진수면 아직 md5예요.
    select count(*) from users where password_hash not like '$2%'; -- 0이어야 정상

  2. 02

    코드 훑기

    git grep -nE "createHash\(('|\")(md5|sha1)" → 비밀번호 근처에 남아 있지 않아야 합니다.

  3. 03

    응답 시간 보기

    로그인 응답이 너무 빠르지 않은지 — bcrypt는 원래 0.1~0.3초 걸립니다. 즉시 응답이면 옛 경로를 타고 있을 수 있어요.

  4. 04

    옛 경로 제거 후 재로그인

    예전 비밀번호로 로그인해 보기 → ③이 제대로 돌았다면 정상 로그인돼야 합니다.

규제 연결

PIPA 제29조 안전조치의무·시행령 제30조는 비밀번호의 일방향 암호화 저장을 명시합니다. ISMS-P 2.7.1(암호화 적용 — 비밀번호 일방향) 항목에 직접 대응하고요. 우리 리포트는 이 발견에 해당 조항을 붙여 "무엇을 위반했고 무엇을 바꿔야 하는지"까지 한국어로 정리합니다.

출처/더 읽기: OWASP A02:2021(Cryptographic Failures), OWASP Password Storage Cheat Sheet, CWE-916, 개인정보 보호법 제29조·시행령 제30조, 개인정보의 안전성 확보조치 기준. 이 카드는 우리가 직접 작성했습니다.

문서 목록으로