이직하거나 새 서비스를 열고, 쓰던 비밀번호 뒤에 느낌표만 붙이기 전에, 많은 사람이 먼저 검색합니다. 이 비밀번호가 유출됐는지. 결과 페이지에는 「비밀번호 강도 검사」「Have I Been Pwned」「온라인 유출 조회」가 나란히 올라오고, 제목만 보면 같은 질문에 답하는 것처럼 보입니다. 열어 보면 입력칸 내용을 자기 서버로 POST하는 페이지가 있고, 제3자 인터페이스에 해시 접미사 목록만 요청하는 페이지가 있고, 공개된 약한 비밀번호 단어표를 받아 이 탭에서 대조하는 페이지가 있습니다. 「비밀번호를 밖으로 보냈는가」에 대한 답이 셋 다 다릅니다.
이 글은 어떤 검사 페이지가 약함·보통·강함을 찍는지 되풀이하지 않고, 남의 데이터베이스를 두드리는 방법도 적지 않습니다. 질문은 하나입니다. 기기에서 공개 목록만 대조하는 것과, 비밀번호를 전체 유출 조회에 맡기는 것은 각각 무엇을 증명하고, 무엇을 증명하지 못하는가. MyPassGen의 강도 검사는 전자입니다. 검사 중인 비밀번호는 올리지 않고, 페이지와 함께 받는 공개 약한 비밀번호 목록과 대조하며, 가입 없이 바로 씁니다. 경계를 펼쳐 두고, 개발자 도구로 그 자리에서 확인합니다.
완전히 다른 세 가지 「비밀번호 확인」
이름을 먼저 맞춥니다. 그렇지 않으면 「로컬」「올리지 않는다」「유출 DB를 봤다」가 마케팅 문장 하나에 같이 들어가고, 가리키는 프로토콜은 셋입니다.
| 방식 | 브라우저를 떠나는 것 | 답할 수 있는 질문 |
|---|---|---|
| 원문을 사이트에 넘김 | 평문, 또는 되돌릴 수 있는 폼 필드 | 상대 서버가 「조회했다」고 말함 — 근거는 안 보임 |
| k-익명성 범위 조회 (HIBP 등) | SHA-1 또는 NTLM 해시의 앞 5자리 16진수 | 그 해시가 상대가 유지하는 전체 유출 집합에 있는가 |
| 기기에서 공개 약한 비밀번호 목록 대조 | 정적 단어표 파일. 입력값은 아님 | 123456, password처럼 반복해 쓰인 비밀번호인가 |
첫째가 가장 편하고, 확인하기도 가장 어렵습니다. 페이지에 「조회 후 바로 삭제」라고 써도 로그, 백업, 분석 스크립트에 사본이 남을 수 있습니다. 둘째는 Have I Been Pwned Pwned Passwords가 공개한 방식입니다. 브라우저가 먼저 해시를 계산하고, 접두사만 api.pwnedpasswords.com/range/{prefix}에 보낸 뒤, 접미사 목록을 받아 이 기기에서 맞춥니다. 셋째는 접두사조차 보내지 않습니다. 단어표는 공개된 흔한 약한 비밀번호이고, 분량은 보통 수백에서 만 줄입니다. 「전 세계 모든 유출」이 아닙니다.
미국 국립표준기술연구소는 NIST SP 800-63B-4에서 검증 쪽이 비밀번호를 만들거나 바꿀 때 「자주 쓰이고, 예측 가능하거나, 이미 유출된」 블랙리스트와 대조하라고 적습니다. 대소문자·숫자·기호를 반드시 섞으라는 규칙은 더하지 말라고도 합니다. 블랙리스트의 주된 용도는 온라인 추측에서 먼저 시도되는 값을 막는 일이고, 목록이 속도 제한 창을 넘을 만큼 커지면 이득은 작다고 적혀 있습니다. 로컬 Top 목록의 자리가 여기입니다. 흔한 약한 비밀번호를 거르는 첫 단계이지, 전체 기록 검색이 아닙니다.
질문을 나눈 뒤에 도구를 고르세요
「사전 공격이 먼저 맞힐까」와 「특정 유출 사건에 나왔나」는 다른 문장입니다. 로컬 목록은 앞 문장에 답합니다. 전체 유출 범위 조회는 뒷문장 가운데 「상대의 이 집합이 담는가」에 답합니다. 어느 쪽도 「여러 사이트에 같은 비밀번호를 쓰지 마라」를 대신하지 못합니다.
원문을 사이트에 넘긴다는 것
아직도 「온라인 비밀번호 검사」 가운데는 입력칸 문자열을 요청 본문에 그대로 넣는 곳이 있습니다. JSON 필드 password로 쓰거나, 쿼리 문자열에 넣거나, 되돌릴 수 있는 인코딩을 한 뒤 POST합니다. 브라우저 입장에서는 로그인 폼과 본질이 같습니다. 상대 호스트, 리버스 프록시, 접속 로그가 이번 제출을 볼 수 있습니다.
HTTPS가 막는 것은 전송 경로의 엿듣는 사람입니다. 상대가 평문을 디스크에 쓰는 일, 분석 스크립트가 필드를 다시 넘기는 일을 막지 않습니다. 줄여야 하는 것은 이번 검사에 내용을 아는 쪽이 한 곳 더 생기느냐이지, HTTPS의 역할을 다시 쓰는 일이 아닙니다. 앞글 브라우저에서 암호화할 때 평문이 올라가지 않았는지 그 자리에서 확인하는 방법이 같은 원칙을 적었습니다. 구호는 증거가 아니고, 트래픽은 증거가 됩니다.
더 잘 안 보이는 넘김도 있습니다. 검사할 비밀번호를 주소창 쿼리에 넣고, 그 링크를 동료에게 보내거나 티켓에 붙이는 일입니다. 쿼리 문자열은 요청 줄에 들어가고 상대 로그에도 남습니다. 일부 제3자 리소스의 Referer는 경로와 query를 그대로 실을 수 있습니다. 검사 값이 실제 계정 비밀번호라면, 이 단계는 스스로 퍼뜨리는 일입니다. 올바른 기본값은 이렇습니다. 실제 비밀번호는 현재 탭의 입력칸에만 두고, URL에 쓰지 않으며, 원문을 올리는 「검사 사이트」에 붙이지 않습니다.
해시 접두사만 보내는 전체 조회
Have I Been Pwned의 범위 인터페이스는 「전체 집합」과 「원문을 넘기지 않음」을 나눕니다. 공식 문서의 순서는 이렇습니다. 이 기기에서 비밀번호를 UTF-8로 두고 SHA-1을 계산합니다. NTLM도 선택할 수 있습니다. 해시의 앞 5자리 16진수를 잘라 GET https://api.pwnedpasswords.com/range/{앞다섯자리}를 요청합니다. 응답은 접미사와 등장 횟수를 콜론으로 나눈 목록입니다. 클라이언트가 이 기기에서 접두사와 접미사를 붙이고, 전체가 맞는지 봅니다. Troy Hunt는 Understanding Have I Been Pwned's Use of SHA-1 and k-Anonymity에서 이 모델을 k-익명성이라 부릅니다. 서버가 보는 것은 해시 버킷이지, 전체 해시도, 비밀번호 원문도 아닙니다.
Cloudflare가 HIBP와 협력하며 공개한 숫자도 그 자리에서 대조할 수 있습니다. 접두사를 5자리로 두면 같은 버킷의 해시 수 중앙값은 약 305, 응답 크기 중앙값은 약 12.2 KB입니다. 공식은 Add-Padding: true도 줍니다. 응답을 대략 800–1000줄로 채워, 「응답이 얼마나 긴지만 보고 흔한 비밀번호인지 짐작하는」 부채널을 줄입니다. 범위 조회는 원문을 POST하는 것보다 절제되어 있습니다. 그래도 나가는 요청은 한 번입니다. Network에 api.pwnedpasswords.com 접속이 보여야 하고, 네트워크를 끊으면 이 조회는 실패해야 합니다.
범위 조회가 「절대 익명」인 것도 아닙니다. 접두사는 관찰자에게 해시가 165개 버킷 중 어디에 떨어지는지 알려 줍니다. 페이지 스크립트가 접두사를 보내기 전에 원문을 따로 저장하면 k-익명성은 도움이 되지 않습니다. 이메일 조회와 비밀번호 조회는 같은 API도 아닙니다. HIBP v3에서 이메일 해시 범위 조회는 앞 6자리를 쓰고, 다른 제품 능력입니다. 이 글은 「이 비밀번호가 유출 비밀번호 집합에 있는가」만 대조합니다. 이메일 구독이나 도메인 감시를 같은 일로 쓰지 않습니다.
따라서 「HIBP를 썼다」를 「비밀번호가 기기를 전혀 떠나지 않았다」로 쓰는 것은 정확하지 않습니다. 정확한 말은 이렇습니다. 원문과 전체 해시는 설계상 상대에게 가지 않습니다. 접두사와 범위 요청 한 번은 떠납니다. 「전체 유출 집합을 유지하는 제3자가 해시 버킷을 알게 되는 것」을 받아들이면, 그것은 타당한 공학적 타협입니다. 「이번 검사에서 접두사조차 나가지 않아야 한다」면 다음 절의 로컬 목록을 쓰고, 다루는 범위가 더 좁다는 점을 인정해야 합니다.
로컬 목록은 단어표만 받고, 비밀번호는 넘기지 않는다
로컬 대조는 순서가 반대입니다. 공개된 약한 비밀번호 목록을 현재 origin으로 받은 뒤, 스크립트에서 집합 조회를 합니다. Network에 단어표 파일이 나타나는 것은 됩니다. 입력칸의 검사 중인 비밀번호가 나타나는 것은 안 됩니다. 단어표 자체는 공개 데이터입니다. 흔한 출처는 OWASP / Daniel Miessler가 유지하는 SecLists의 「천만 개 비밀번호에서 Top N을 자른」 파일입니다. 답하는 질문은 「공격자 사전이 먼저 시도할까」이지, 「공개되지 않은 어떤 유출에 있는가」가 아닙니다.
MyPassGen의 검사는 이 경계로 동작합니다. 페이지를 열면 data/leaked-top10k.txt를 요청합니다. 현재 약 860줄이고, 한 줄에 소문자 비밀번호 하나, 앞쪽은 123456, password, qwerty처럼 반복되는 약한 값입니다. 대조는 브라우저에서 끝납니다. 소문자로 바꾼 뒤 제한된 변형을 만듭니다. @ / 4를 a로, 0을 o로 보는 흔한 leet, 끝의 숫자 한 자리에서 세 자리를 떼는 처리입니다. 집합에 들어가면 길이가 길어 보여도 약함입니다. 단어표를 못 받으면 스크립트는 내장 최소 집합(password, 123456 등)으로 돌아가고, 외부 유출 인터페이스를 부르지 않습니다.
강도 평가는 다른 로컬 계산입니다. 문자 풀은 소문자, 대문자, 숫자, 기호 포함 여부로 더합니다(26 + 26 + 10 + 32). 엔트로피는 대략 길이에 log2(문자 풀)을 곱한 값이고, 같은 문자가 너무 많으면 깎습니다. 등급 임계값은 약 40비트 미만이 약함, 60 미만이 보통, 80 미만이 강함, 그 이상이 매우 강함입니다. 길이가 8자 미만이면 따로 약하다고 알립니다. 무작위 생성은 6–128자를 허용하고 기본은 16자입니다. 오프라인 무차별 대입은 초당 1010회, 온라인 속도 제한은 초당 103회로 어림합니다. 자기 자신을 위한 자릿수이지, 특정 GPU에 대한 약속이 아닙니다. 분석 이벤트는 등급과 목록 적중 여부만 남기고, 비밀번호 원문을 통계로 보내지 않습니다.
이 숫자는 이 기기에서 맞출 수 있습니다. 단어표 줄 수는 편집기나 wc -l로 셉니다. Network에는 단어표가 보여야 하고, 입력값은 보이면 안 됩니다. 그래도 「전 세계를 조회했다」고 쓸 수는 없습니다. 860줄은 SecLists가 공개한 Top 10,000도 다 담지 못하고, HIBP처럼 해시로 모은 전체 유출 비밀번호 집합은 더더욱 아닙니다. 맞지 않았을 때 정직한 결론은 하나뿐입니다. 이 목록에 있는 흔한 약한 비밀번호가 아니다.
목록에 없다고 「안전」이나 「한 번도 유출되지 않음」으로 쓰지 마세요
로컬 목록이 막는 것은 사전 맨 앞의 한 묶음입니다. 목록 밖의 유출, 조합 대입, 그 사이트만을 노린 사회공학 추측에 비밀번호가 있을 수 있습니다. 새 비밀번호가 필요하면 이 기기에서 새로 만들고, 원래 단어 뒤에 연도나 느낌표를 붙여 계속 쓰지 마세요.
강도 막대는 유출을 증명하지 않는다
강도 막대가 어림하는 것은 전수 조사 공간이지, 기록 검색이 아닙니다. 20자 무작위 비밀번호의 엔트로피는 높을 수 있습니다. 이미 남의 유출 파일에 있으면 공격자는 문자 집합부터 추측하지 않고, 있는 목록으로 바로 넣습니다. 반대로 Password1!은 「대소문자, 숫자, 기호가 있어야 한다」는 폼을 자주 통과하고, 공개된 약한 비밀번호 목록에는 거의 들어갑니다. NIST가 조합 규칙을 더 이상 요구하지 않는 근거가 이런 우회가 흔하다는 점입니다.
그래서 검사 페이지가 강도와 목록 결과를 같이 주면, 목록이 강도를 덮어야 합니다. 적중하면 약함입니다. 강도 막대는 여전히 쓸모가 있습니다. 「너무 짧다, 문자 종류가 하나다, qwerty 같은 키보드 수열이다」를 알립니다. 「계속 써도 된다」는 증명을 혼자 발급하지는 못합니다. 보통 필요한 결정은 두 문장입니다. 목록에 걸리거나 8자 미만이면 바로 바꿉니다. 목록에 없고 충분히 길어도 사이트마다 나누어 두고, 중요한 계정은 비밀번호 관리자나 이 기기의 생성기로 따로 씁니다.
「전체 조회에서 안 맞았다」도 끝이 아닙니다. 범위 조회는 상대 집합의 수록 범위와 갱신 주기에 의존합니다. 집합 밖의 비공개 유출, 내부 시스템 비밀번호, 아직 해시로 실리지 않은 변형은 나타나지 않습니다. 로컬 860줄보다 훨씬 넓고, 그래도 남이 유지하는 표본입니다. 두 도구를 겹쳐 써도 됩니다. 전제는 각각이 무엇을 내보내고, 실패가 무엇을 뜻하는지 나눌 수 있어야 한다는 것입니다.
그 자리 확인: 단어표는 되고, 비밀번호는 안 된다
아래 단계는 브랜드 약속에 기대지 않습니다. 버려도 되는 시험 비밀번호를 씁니다. 예를 들어 password, 또는 어떤 계정에도 쓴 적 없는 임시 문자열입니다. 지금 쓰는 실제 비밀번호를 시연에 붙이지 마세요.
- 개발자 도구 Network를 열고 「로그 보존 / Preserve log」를 켭니다. 필터는 먼저 Fetch / XHR, 이어서 문서와 다른 요청을 한 바퀴 봅니다.
- 검사 페이지를 새로고침합니다. 정적 목록이 보여야 합니다. 이 사이트는
leaked-top10k.txt입니다. 그 요청을 엽니다. 응답은 한 줄에 비밀번호 하나이고, 요청 본문은 비어 있어야 합니다. - 입력칸에 시험 비밀번호를 넣고 결과가 나올 때까지 기다립니다. 새로 생긴 요청을 봅니다. URL, query, JSON, 폼 필드 어디에도 방금 친 문자열이 없어야 합니다.
- 검색 칸에
pwnedpasswords,hibp,range/를 넣습니다. 로컬 목록 방식이면 이 호스트가 나오면 안 됩니다. 나오면 페이지가 범위 조회를 탄 것이고, 앞 절의 기준으로 읽어야 합니다. - 분석 요청도 봅니다. 경로에
matomo,collect가 자주 있습니다. 이벤트 이름에 「약함 / 보통 / 강함」은 될 수 있습니다. 값에 비밀번호 원문은 없어야 합니다. - 「전체 집합 방식」과 대조하려면, 같은 버려도 되는 비밀번호로 HIBP를 부르는 페이지를 한 번 봅니다.
api.pwnedpasswords.com/range/로의 GET이 보여야 하고, 경로에는 16진수 5자리만 있고 원문은 없어야 합니다.
네트워크를 끊고 한 번 더 입력하는 것은 로컬 방식의 가산 확인입니다. 단어표가 이미 캐시되어 있으면 강도와 적중은 계산되어야 하고, 범위 조회는 실패해야 합니다. 「요청이 전혀 없다」를 유일한 합격선으로 두지 마세요. 처음 열 때는 단어표를 받아야 하고, 그것은 비밀번호를 POST하는 트래픽과 종류가 다릅니다. 앞글의 로컬 암호화도 같은 패널을 썼습니다. 여기서는 한 가지만 더 봅니다. 업무 요청에 내 입력이 있는가.
오해: 초록 막대, 전체 조회, 「확인했으니 안전」
「강도 막대가 초록이면 유출되지 않았다」는 성립하지 않습니다. 초록 막대는 문자 공간을 말하고, 유출은 남이 같은 문자열을 이미 가지고 있는지를 말합니다. 둘은 동시에 참일 수 있습니다.
「로컬 계산이라고 적혀 있으니 나가는 요청이 없다」도 성립하지 않습니다. 로컬 목록은 단어표를 받습니다. 범위 조회는 어떤 해시 버킷을 받습니다. 막아야 하는 것은 원문과 전체 해시가 업무 데이터로 떠나는 일이지, 모든 HTTP가 아닙니다.
「HIBP에 없으니 모든 사이트에 재사용해도 된다」가 가장 비싼 오해입니다. 자격 증명 채워 넣기 공격은 「한 비밀번호로 많은 사이트」를 쓰고, 그 비밀번호가 공개 순위 천 위 안에 있을 것을 요구하지 않습니다. 로컬 목록과 전체 조회 모두 재사용을 풀지 못합니다. 새 비밀번호는 이 기기에서 만들고, 무작위 모드에서는 기본 16자 이상을 쓴 뒤, 사이트마다 따로 넣습니다.
「채팅에 붙여 동료에게 강한지 물어보기」는 검사를 전파로 바꿉니다. 새 비밀번호를 남에게 넘겨야 하면, 한 번만 쓰는 짧은 비밀은 일회용 링크로 가고 키는 URL의 # 뒤에 둡니다. 반복해 풀 파일은 파일 암호화로 갑니다. 검사 페이지가 푸는 일은 「스스로 목록과 강도를 먼저 보는 것」이지, 키 교환이 아닙니다.
어디서부터 확인하나
버리려는 비밀번호, 또는 실제 계정에 쓴 적 없는 값 하나를 먼저 봅니다. 검사 페이지를 열고 Network의 단어표 요청을 본 뒤, 입력하고 그 문자열이 나가지 않았는지 확인합니다. 목록에 걸리면 글자 두 개만 바꿔 계속 쓰지 말고, 비밀번호 생성에서 이 기기로 새로 만듭니다. 길이는 16자 이상. 목록에 없어도, 이 공개 약한 비밀번호 집합의 구성원이 아니라는 뜻일 뿐이고, 사이트마다 나누어 둡니다.
질문이 사실은 「이 비밀번호가 알려진 대규모 유출에 나왔나」라면, 로컬 800여 줄로는 부족합니다. k-익명성 범위 조회를 타고, Network에서 「16진수 5자리 접두사만 보냈다」를 볼 수 있는 도구를 쓰세요. 출처를 모르는 조회 페이지에 원문을 넘기지 마세요. 두 확인을 다 해도, 열면 바로 쓰는 검사가 각 사이트의 로그인 보호를 대신하지 않고, 서버가 나중에 다시 유출되지 않는다고 증명하지도 않습니다.
이 한 줄을 끝내면 제목의 차이에 이미 답할 수 있습니다. 기기에서 유출 목록을 대조하면 「공격자가 먼저 시도하는 묶음처럼 생겼는가」를 증명합니다. 전체 비밀번호 조회는 — 구현이 맞다면 — 「상대의 이 해시 집합에 전체가 맞는가」를 증명합니다. 전자는 단어표가 나가고 비밀번호는 나가지 않습니다. 후자는 접두사가 나가고 원문은 나가지 않습니다. 원문을 POST하는 셋째는 둘 중 어느 쪽도 아닙니다.
자주 묻는 질문
로컬에서 안 맞으면 Have I Been Pwned도 봐야 하나요?
어떤 답을 원하는지에 달렸습니다. 안 맞은 것은 이 공개 약한 비밀번호 목록의 구성원이 아니라는 뜻입니다. HIBP가 수록한 유출 비밀번호 해시 집합에 있는지도 알고 싶으면 범위 조회를 따로 하고, Network에서 5자리 접두사만 보냈는지 확인합니다. 두 일은 나눌 수 있습니다. 「이미 전 세계를 봤다」로 섞지 마세요.
범위 조회가 내 비밀번호를 HIBP에 알리나요?
공식 설계와 Troy Hunt의 설명대로면, 상대가 받는 것은 SHA-1 또는 NTLM 해시 앞 5자리이지, 비밀번호 원문도 전체 해시도 아닙니다. 일치는 브라우저에서 끝납니다. 그래도 나가는 요청은 한 번입니다. 접두사조차 기기를 떠나면 안 되면 로컬 목록만 쓰고, 다루는 범위가 더 좁다는 점을 받아들입니다.
강도 검사에 가입이 필요한가요? 검사 중인 비밀번호가 올라가나요?
가입은 필요 없습니다. 로컬 방식이라면 검사 중인 비밀번호는 입력칸에 남아야 하고, 브라우저를 떠나는 것은 단어표 파일과 원문이 없는 분석 이벤트여야 합니다. 됐는지는 Network가 기준이고, 페이지의 「올리지 않는다」가 기준이 아닙니다.
강도의 「오프라인으로 깨려면 N년」을 믿어도 되나요?
고정된 추측 속도로 만든 자릿수 힌트이지, 특정 그래픽카드나 특정 단어표 공격의 측정이 아닙니다. 목록에 걸리면 공격자는 그 시계대로 전수 조사하지 않습니다. 「너무 짧다 / 문자 종류가 적다」의 참고로 보고, 안전 보증으로 보지 마세요.