kbstar.com 과 kb5tar.com. 나란히 놓으면 다르다는 걸 알지만, 문자로 받은 링크를 누르기 직전 주소창을 흘끗 볼 때는 알아채기 어렵습니다.
공식 사이트 주소 디렉터리를 만들면서 "이 주소가 맞느냐"에 답하는 기능이 필요해졌고, 그 판별 로직을 정리했습니다.
- 설계 제약: 입력된 주소로 HTTP 요청을 보내지 않는다. 판별 대상이 피싱 사이트일 수 있어 서버 로그에 흔적을 남기지 않기 위해서이고, 사용자 입력을 그대로 fetch 하면 SSRF가 됩니다. 호스트 파싱과 DB 비교만으로 끝냅니다.
- uri.Host 대신 uri.IdnHost: 한글·유니코드 도메인을 퓨니코드로 정규화해야 같은 도메인이 두 표기로 갈라지지 않습니다. 주소핀.com → xn--9l4b19kg3k.com
- 등록 가능 도메인 단위 비교: 사칭 주소는 앞부분을 그럴듯하게 채웁니다. naver.com-login.xyz 의 등록 도메인은 com-login.xyz 입니다. 뒤쪽이 목적지를 결정합니다.
- 숫자 치환 정규화(0→o, 1→l, 3→e, 4→a, 5→s) + 하이픈·밑줄 제거. 한 줄짜리 Replace 체인인데 실제로 가장 많은 유형을 잡습니다. 정교한 알고리즘보다 도메인 지식이 먼저였습니다.
- 포함 관계를 편집 거리보다 먼저: kbstar-login → kbstarlogin 은 kbstar 를 통째로 포함합니다. 편집 거리로는 5지만 명백한 사칭 후보입니다. 단 브랜드 길이 4 이상일 때만(두 글자 브랜드로 포함 검사하면 오탐 폭발).
- 레벤슈타인 임계값을 브랜드 길이에 따라 다르게: toss/boss, naver/never 가 전부 거리 1입니다. 짧은 브랜드에 2를 허용하면 무관한 도메인이 마구 걸립니다.
- 판정을 Unknown 과 Lookalike 로 분리한 이유: "목록에 없다"와 "공식과 비슷한데 일치하지 않는다"는 다릅니다. 전자를 위험하다고 말하면 멀쩡한 사이트를 가짜로 모는 셈입니다.
- 판정 로직을 static 으로 분리해 DB 없이 테스트: 경계 조건(짧은 브랜드 오탐, 숫자 치환, 서브도메인 사칭)을 마음껏 흔들어 볼 수 있습니다.
결국 어려웠던 건 알고리즘이 아니라 "어디까지를 비슷하다고 할 것인가"와 "모르는 주소를 위험하다고 말하지 않기"였습니다.
ASP.NET Core 10 Razor Pages, PostgreSQL. 정규화 규칙에서 빠진 치환이나 더 나은 접근이 있으면 알려 주시면 반영하겠습니다.