비밀번호를 SHA-256으로 해싱했다가 유출 사고에서 크게 후회한 이유
문제 발생
비밀번호를 평문으로 저장하면 안 된다는 것은 알고 있어서 SHA-256으로 해싱해 저장했는데, 보안 검토에서 이 방식이 위험하다는 지적을 받았습니다 — SHA-256도 되돌릴 수 없는 해시 함수인데 왜 문제인지 이해가 필요했습니다.
원인 분석
SHA-256은 암호학적으로 안전한 해시 함수이지만, 비밀번호 해싱 용도로 설계된 함수가 아닙니다. SHA-256의 원래 설계 목적(파일 무결성 검증, 디지털 서명 등)은 오히려 빠르게 계산되는 것이 장점입니다 — 현대 하드웨어(특히 GPU)는 SHA-256을 초당 수십억 번 계산할 수 있습니다.
이 "빠름"이 비밀번호 해싱에서는 정확히 약점이 됩니다. 만약 해시된 비밀번호 데이터베이스가 유출되면, 공격자는 흔한 비밀번호 목록이나 무차별 대입으로 각 후보를 SHA-256으로 해싱해보며 원본 데이터베이스의 해시와 비교하는 작업을 엄청나게 빠른 속도로 반복할 수 있습니다 — 특히 무작위가 아닌 흔한 비밀번호(password123 등)를 쓴 계정은 초 단위로 뚫릴 수 있습니다. Rainbow table(미리 계산해둔 해시 테이블) 공격까지 고려하면 salt 없는 SHA-256은 더욱 취약합니다.
해결 방안
비밀번호 해싱에는 의도적으로 느리게, 그리고 조정 가능한 비용(cost factor)을 갖도록 설계된 전용 함수를 씁니다 — bcrypt, scrypt, Argon2가 대표적입니다.
import bcrypt from "bcrypt";
const hash = await bcrypt.hash(password, 12); // cost factor 12
const isValid = await bcrypt.compare(inputPassword, hash);- 속도를 의도적으로 늦춘 것 자체가 방어책입니다 — bcrypt의 cost factor를 높이면 정상적인 로그인 검증(한 번의 계산)에는 몇십~몇백 밀리초 정도의 무시할 만한 지연만 생기지만, 공격자가 수백만 개의 후보를 시도해야 하는 무차별 대입 공격에는 그 지연이 누적되어 현실적으로 불가능한 시간이 걸리게 만듭니다.
- Salt가 자동으로 처리됩니다 — bcrypt/Argon2는 해시마다 무작위 salt를 자동으로 생성해 포함시키므로, 같은 비밀번호를 쓰는 두 사용자의 해시값이 겹치지 않고, rainbow table 공격도 무력화됩니다. 직접 SHA-256 + 수동 salt를 구현하는 것보다 훨씬 안전하고 실수할 여지가 적습니다.
- 이 프로젝트는 Better Auth를 인증 라이브러리로 쓰고 있으므로, 비밀번호 해싱 알고리즘 자체를 직접 선택/구현할 필요가 없습니다 — 라이브러리가 이미 안전한 기본값(scrypt 계열)을 쓰도록 구성되어 있고, 직접 해싱 로직을 작성하는 것 자체가 불필요한 위험을 만드는 일입니다. 자체 구현이 필요한 상황이 아니라면 검증된 라이브러리/프레임워크의 기본 구현을 그대로 신뢰하는 것이 최선입니다.
- cost factor(작업 계수)는 하드웨어 성능이 올라감에 따라 주기적으로 재검토해서 올려야 합니다 — 몇 년 전 "충분히 느렸던" 값이 지금 하드웨어 기준으로는 부족할 수 있습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.