이 브라우저에서 실제로 관찰 가능한 신호(쿠키, HTTP 헤더, 브라우저 지문, 행동 패턴)를 수집해 Session ID / Cluster ID를 만들고, risk score와 정책(Allow~Block)을 계산합니다.
docs/research/browser-identity-bot-management-signals.md 기반User-Agent, TLS/HTTP 스택 흉내, 화면 크기, timezone까지 정상 브라우저처럼 맞춘 고급 자동화가 존재하기 때문에 단일 신호로는 판정하지 않는다. Akamai Bot Manager, Cloudflare Bot Management 같은 상용 솔루션도 아래처럼 계층을 쌓아 앙상블로 판단한다.
| 계층 | 신호 | 이 프로토타입 구현 | 우회 난이도 |
|---|---|---|---|
| 1. IP 평판 | ASN, 데이터센터/프록시 여부 | 구현 ip-api.com 조회 | 낮음 (비용 발생) |
| 2. TLS 지문 (JA3/JA4) | ClientHello 암호화 스위트·확장 순서 | 미구현 앱 서버 레이어 한계 | 매우 높음 |
| 3. HTTP 지문 | 헤더 개수·순서, Client Hints | 근사 구현 sec-ch-ua 유무만 체크 | 중간 |
| 4. 브라우저 지문 | Canvas, WebGL, AudioContext, webdriver | 구현 | 중간 (stealth plugin으로 부분 우회) |
| 5. 쿠키/클러스터 연속성 | 세션 재발급 패턴, 장기 클러스터 | 구현 Session ID + Cluster ID | 낮음~중간 |
| 6. 행동 패턴 | 마우스/스크롤/키보드 이벤트 | 근사 구현 2.5초 짧은 창 | 중간 |
단일 신호가 아니라 앙상블이기 때문에, 하나(예: UA)를 속여도 나머지(예: webdriver 플래그, WebGL renderer)에서 걸린다. 자세한 매핑은 docs/bot-identification-scoring-model.md 참고.
TLS 지문은 클라이언트가 서버에 보내는 ClientHello 메시지의 필드(TLS 버전, 암호화 스위트 순서, 확장 목록, 타원 곡선)를 해시한 값이다. HTTP 레이어가 아니라 TLS 핸드셰이크 레이어에서 결정되므로 User-Agent를 바꿔도 우회되지 않는다.
문제는 이 값이 raw ClientHello 바이트를 TLS 협상이 끝나기 전에 캡처해야 나온다는 점이다. Node.js의 http/https 모듈은 이미 협상이 끝난 소켓만 애플리케이션 코드에 넘겨주므로, 이 프로토타입처럼 앱 서버 레벨에서 동작하는 한 JA3를 관찰할 방법이 없다. 실제로 이걸 관찰하려면 nginx/Envoy/CDN Edge처럼 TLS를 직접 종단하는 레이어가 필요하다 — 원본 리서치 문서가 "TLS/HTTP fingerprint"를 별도 섹션으로 다루는 이유이자, M2가 이 기능을 앱이 아니라 Edge/Proxy에 둬야 하는 이유다.
"어디서" 캡처할지는 후보가 여러 개이고, 난이도 차이가 크다. 목적을 "요청 단위로 앱이 읽을 수 있는 HTTP 헤더로 만든다"로 고정하면 비교가 명확해진다.
| 방식 | 난이도 | 왜 그런가 |
|---|---|---|
| Envoy 커스텀 listener filter | 높음 | ClientHello를 들여다보는 지점(listener filter)은 네이티브(C++)만 확장 가능. WASM/Lua로는 이 단계에 못 붙어서 커스텀 Envoy 빌드가 필요 |
nginx stream + ssl_preread | 중간 (관찰만 하면 낮음) | $ssl_preread_cipher_list 등을 변수로 바로 주지만, 이건 stream(L4) 컨텍스트 값이라 HTTP 요청 헤더로 그대로 못 넘어간다. PROXY protocol에 임의 TLV를 넣는 것도 오픈소스 nginx 표준 기능이 아니라서, 결국 IP:port로 상관관계를 맞추는 별도 프로세스가 필요해짐 |
| 작은 Go TCP 프록시가 TLS 직접 종단 | 낮음 | Go는 TLS 핸드셰이크 콜백(tls.Config.GetConfigForClient)에서 파싱된 ClientHello 정보를 구조체로 바로 넘겨줌 → 같은 프로세스 안에서 "파싱 → HTTP 헤더로 재조립"이 끝남 |
| CDN/WAF가 이미 제공 (Cloudflare 등) | 매우 낮음 | 해당 벤더 뒤에 있다면 재구현 불필요, 벤더가 계산한 값을 헤더로 전달받기만 하면 됨 |
| 방식 | 난이도 | 얻는 것 | 한계 |
|---|---|---|---|
표준 라이브러리 GetConfigForClient 콜백 | 매우 낮음 (~80줄, 외부 의존성 0) | ClientHelloInfo에서 CipherSuites, SupportedCurves, SupportedPoints, ALPN, SNI를 구조체로 그대로 획득 | 진짜 JA3는 아님 — extension ID 순서까지 반영한 표준 해시가 아니라 근사치 |
raw ClientHello 바이트 파싱 (예: github.com/dreadl0ck/ja3) | 중간 | 표준 JA3 MD5 해시 그대로 계산 → 공개 JA3 블랙리스트(Bad Packets, abuse.ch 등)와 직접 대조 가능 | 라이브러리 추가 필요, TLS 종단 전 바이트를 가로채는 net.Conn 래퍼 구현 필요 |
실무 권장 순서: 근사치(표준 라이브러리)로 먼저 신호를 채우고, 공개 JA3 블랙리스트 대조가 실제로 필요해지는 시점에 raw 파싱으로 업그레이드한다.
JA3를 결정 신호로 쓰면 안 되는 실질적인 이유는, 흉내내는 두 경로가 이미 진입장벽이 낮기 때문이다.
| 우회 방식 | 난이도 | 왜 되는가 | 대신 어디서 잡히나 |
|---|---|---|---|
| Playwright/Puppeteer/Selenium으로 진짜 브라우저 구동 | 낮음 | TLS 스택 자체가 진짜 Chrome/Firefox 것이라 JA3가 사람과 100% 동일 — 흉내가 아니라 진짜임 | navigator.webdriver, headless WebGL software renderer, 행동 이벤트 부재 (Browser Fingerprint / Behavior 카테고리) |
| uTLS 계열 등 TLS 스택 흉내 라이브러리 | 낮음 | Chrome/Firefox ClientHello 템플릿(암호화 스위트 순서, 확장 목록, GREASE 값)을 하드코딩 — 스크레이핑 커뮤니티에 성숙한 오픈소스 도구가 이미 널려 있음 | HTTP/2 프레임 지문(SETTINGS 값, HEADERS 우선순위)까지 정확히 맞추기는 훨씬 어려움. 대량 운영 시 IP 로테이션·쿠키 리셋 패턴이 장기 클러스터링에서 드러남 |
그래서 Akamai/Cloudflare 같은 상용 솔루션도 JA3 단독이 아니라 JA3 + HTTP/2 지문을 같이 본다 — TLS만 흉내낸 라이브러리가 HTTP/2 레이어까지 브라우저와 완전히 똑같이 동작하게 만들기는 훨씬 어렵기 때문이다. "진짜 브라우저 자동화"는 반대로 TLS/HTTP2는 완벽히 통과하지만 JS 레이어(webdriver, WebGL, 행동 패턴)에서 걸린다. 결론적으로 어느 한 카테고리도 단독으로는 못 막고, 여러 카테고리를 합산하는 이 프로토타입의 스코어링 구조가 맞는 방향이다. 원본 문서 2장의 "Advanced Evasion Pressure" 섹션과 같은 결론이다.
| 신호 | 정상 브라우저 | 자동화/헤드리스 |
|---|---|---|
navigator.webdriver | false 또는 속성 없음 | Selenium/Playwright/Puppeteer 기본값 true |
| plugins / mimeTypes | 보통 1개 이상 | headless 환경은 0개인 경우가 많음 |
| WebGL renderer | 실제 GPU 이름 (Apple GPU, Intel Iris 등) | SwiftShader, llvmpipe 같은 소프트웨어 렌더러 |
| UA vs 터치 지원 | 모바일 UA면 touch 지원 있음 | 모바일 UA 스푸핑 + 데스크톱 환경 → 불일치 |
| Canvas / AudioContext hash | GPU/오디오 스택별 미세한 렌더링 차이 존재 | 동일 headless 이미지에서 계속 같은 해시 반복 → 클러스터로 묶임 |
puppeteer-extra-plugin-stealth 같은 도구는 navigator.webdriver를 숨기고 일부 속성을 패치할 수 있지만, WebGL이 실제로 소프트웨어 렌더러를 쓰고 있다는 사실 자체는 GPU가 없는 한 바꿀 수 없다. 그래서 이 프로토타입은 여러 지표를 동시에 봐서 하나를 속여도 다른 지표에서 걸리게 설계했다.
쿠키 하나만 ID로 쓰면, 봇 운영자가 차단당할 때마다 쿠키(또는 프로필)를 새로 만들어 우회할 수 있다. 원본 문서 9장(Long-Term Cluster)의 핵심도 "IP/쿠키 단위가 아니라 fingerprint cluster 단위로 정책을 적용하라"는 것이다.
이 프로토타입에서는 메모리 Map으로 구현했지만, 실제 서비스에서는 Redis 등 TTL이 있는 저장소가 필요하다.
Googlebot/Bingbot 같은 "알려진 좋은 봇"은 User-Agent가 아니라 네트워크 신원으로 검증해야 한다. UA 문자열은 누구나 복사할 수 있기 때문이다. Google/Bing이 공식적으로 권장하는 방법은 다음과 같다.
1번만 확인하면 위조된 hostname을 스스로 등록한 공격자에게 속을 수 있어 4번 정방향 재검증이 필수다. Akamai/Cloudflare도 이런 검증을 거친 크롤러만 IP 화이트리스트에 등록해 "Verified Bot"으로 분류하고 TLS/JS 검사를 생략시킨다.
지금까지 다룬 IP평판/TLS/HTTP/브라우저지문/쿠키 5개 카테고리 외에 Akamai Bot Manager 같은 상용 솔루션이 구조적으로 더 갖는 것들이 있다.
| 요소 | 내용 | 이 프로토타입과 차이 |
|---|---|---|
_abck 쿠키 | JS 센서가 세션 내내 계속 마우스 궤적 곡률/가속도, 이벤트 디스패치 타이밍 jitter, 모바일 자이로/가속도계까지 수집해 암호화 → 매 요청마다 서버가 복호화 검증 | 이 프로토타입은 페이지 로드 시 2.5초 스냅샷 1회뿐. Akamai는 누적 신뢰도, 우리는 단발 판정 |
| 폴리모픽 난독화 | 센서 수집 JS 코드 구조를 주기적으로 변경 — 우회법이 공개돼도 다음 난독화 사이클에서 무력화 | 우리 스코어링 로직은 고정 코드 + 공개 가중치 |
| 크로스 고객사 네트워크 효과 | 수만 개 고객사 트래픽을 동시에 관측 — 봇넷이 A사 공격 후 인프라를 재사용해 B사를 공격하면 즉시 탐지 | 단일 회사가 자기 로그만 보고는 절대 얻을 수 없는 관측 규모. 알고리즘 격차가 아니라 데이터 규모 격차 |
| 실시간 대응 루프 | 공개된 우회 기법이 퍼지면 수일 내로 타겟 대응(난독화 갱신, 우회 도구 시그니처 탐지) 배포 | 정적 룰셋이 아니라 지속적인 공격-방어 사이클 |
| 네이티브 앱 SDK | iOS/Android 앱에 내장 — 탈옥/루팅, Frida/Xposed 후킹, 에뮬레이터 탐지 | 브라우저 기반 지문 수집으로는 아예 손댈 수 없는 영역 |
하위~중급 자동화는 막힌다. 단순 스크립트, curl/requests 기반 스크레이퍼, 세션 관리 없는 크리덴셜 스터핑 — 지금 프로토타입 수준의 다중 카테고리 합산 스코어링으로도 충분히 걸러진다. 원본 문서가 처음부터 "완벽한 식별"이 아니라 "저품질 자동화를 낮은 비용으로 거른다"를 목표로 잡은 이유다.
고급/타겟형 자동화는 막히지 않는다. 실제 Chrome + stealth plugin + residential proxy + 사람처럼 짠 행동 스크립트 조합이면, 알고리즘을 아무리 정교하게 짜도 단일 조직 구현으로는 못 막는다. 여기서 진짜 격차는 "알고리즘이 더 똑똑해서"가 아니라 위 표의 크로스 고객사 데이터 — 순수한 관측 규모의 문제다. 그래서 목표를 "차단"이 아니라 "이 타겟을 공격할 가치가 없을 만큼 운영 비용을 올린다"로 잡는 게 현실적이다.
Akamai가 공식 스펙을 공개하지 않아 아래 내용은 보안 리서치 커뮤니티가 리버스엔지니어링으로 관찰·정리한 내용이다. 정확한 암호화 알고리즘·포맷은 Akamai가 주기적으로 바꾸므로 시점에 따라 달라질 수 있다.
_abck 쿠키 발급/갱신_abck 쿠키 포함 필수 → Edge가 서버 키로 복호화 → 내부 플래그 검증값 자체는 사람이 읽을 수 없는 불투명한 토큰이지만, 커뮤니티 리서치에서는 ~로 구분된 여러 세그먼트(세션 식별자 성격의 해시, 타임스탬프, 상태 플래그)로 구성되고, 마지막 세그먼트(예: ~-1~, ~0~, ~1~)가 "이 세션이 봇으로 플래그됐는지"를 유추하는 경험적 신호로 관찰돼왔다. Akamai가 공식 문서화한 스펙이 아니므로 언제든 바뀔 수 있다는 점을 감안해야 한다.
우리 Session ID(bm_sid)는 서버가 발급하지만 평문 UUID라 클라이언트가 값을 볼 수 있고(그래도 위조는 못 함, 서버 발급 값이므로), 무엇보다 한 번의 스냅샷이다. _abck는 (1) 클라이언트가 절대 못 읽는 암호화된 내용이고 (2) 세션 내내 계속 갱신되는 연속 관찰이라는 두 가지가 다르다.
KV DB(Redis 등) 도입은 "인메모리 → 영속/공유" 격차만 메운다 — 클라이언트가 {mousemove: 100}처럼 자기신고 값을 그냥 지어내 보내는 스푸핑 문제는 저장소를 바꿔도 그대로 남는다. (1)을 흉내내려면 서버가 독립적으로 검증 가능한 신호(IP/헤더/TLS)의 가중치를 자기신고 행동 신호보다 높게 유지하는 절충이 필요하고, (2)를 흉내내려면 클라이언트가 페이지 로드 시 1회가 아니라 주기적으로(예: 스크롤/클릭 이벤트마다, 또는 15초 간격) 다시 POST하도록 바꿔서 KV DB에 누적해야 한다.
봇 판정 시스템에서 가장 치명적인 버그는 "탐지를 못 하는 것"보다 공격자가 제어하는 입력을 신뢰해서 판정을 통째로 우회당하는 것이다. 이 프로토타입도 코드 리뷰에서 이런 우회 구멍이 발견돼 아래처럼 막았다. 각 신호가 "누가 값을 정하는가"를 기준으로 신뢰 등급을 나누는 게 핵심이다.
| 신호 | 값을 정하는 주체 | 신뢰 등급 |
|---|---|---|
소켓 peer IP (req.socket.remoteAddress) | TCP 스택 (위조 불가) | 신뢰 가능 |
| 정/역방향 DNS 검증 결과 | DNS 인프라 (IP 소유권 증명) | 신뢰 가능 |
| IP 평판 조회 (ip-api) | 외부 DB | 부분 신뢰 |
X-Forwarded-For 헤더 | 클라이언트 (누구나 지정) | 위조 가능 |
| User-Agent 헤더 | 클라이언트 | 위조 가능 |
| 클라이언트 JS 지문/행동값 (POST 바디) | 클라이언트 | 위조 가능 |
X-Forwarded-For는 클라이언트가 임의로 넣을 수 있는 헤더다. 이 값을 클라이언트 IP로 그대로 쓰면 두 가지 우회가 동시에 열린다.
X-Forwarded-For: 66.249.66.1 (Googlebot 공개 IP)X-Forwarded-For: 127.0.0.1 + ?testIp=<크롤러IP>방어: 클라이언트 IP의 진실은 위조 불가능한 소켓 peer(req.socket.remoteAddress)뿐이다. XFF는 기본적으로 무시하고, 실제 신뢰 가능한 프록시 뒤에 배포할 때만 TRUST_XFF=1 환경변수로 활성화한다. 이때도 프록시가 XFF를 덮어써야(append가 아니라 replace) 클라이언트가 앞에 위조 값을 끼워넣는 걸 막을 수 있다.
로컬에서 크롤러 검증을 시연하려고 연 ?testIp= 오버라이드가, 아무 가드가 없으면 원격 스크레이퍼가 ?testIp=<크롤러IP>만 붙여 Allow를 얻는 통로가 된다.
방어: testIp는 실제 소켓 peer가 로컬/사설망일 때(=개발자 본인)만 적용한다(XFF가 아니라 소켓 peer로 판정). 응답의 request.testIpApplied로 적용 여부가 노출되고, UI에도 "testIp 오버라이드(로컬 데모)" 배지가 표시된다. 운영에서는 파라미터 자체를 제거하는 게 안전하다.
이 도구는 적대적 트래픽을 들여다보는 화면이다. 봇이 User-Agent나 WebGL renderer 값에 <img src=x onerror=...> 같은 페이로드를 넣고, 운영자가 그 세션을 대시보드에서 열면 운영자 브라우저에서 스크립트가 실행된다. 관측 대상의 값이 곧 공격 벡터가 되는 구조다.
방어: 서버가 돌려준 모든 클라이언트/외부 유래 문자열을 innerHTML에 넣기 전에 HTML 이스케이프(esc())한다. Session/Cluster ID처럼 서버가 생성한 값은 textContent로 출력한다.
POST 바디의 행동 카운트(mousemove 등)나 navigator 값은 봇이 얼마든지 지어낼 수 있다. 그래서 서버가 독립적으로 검증 가능한 신호(소켓 IP, 헤더 존재/순서, DNS)의 가중치를 자기신고 신호보다 구조적으로 높게 둬야 한다. 이 프로토타입도 webdriver/WebGL 같은 자기신고 신호 하나만으로 차단하지 않고 여러 카테고리를 합산하는 이유가 여기 있다.
413 Payload Too Large 반환 — 메모리 고갈 방지.null로 두어, 신호 없는 클라이언트끼리 한 클러스터로 뭉쳐 "reset 패턴" 오탐이 나는 것을 방지.실제 Playwright(Chromium 149)로 이 배포 페이지에 4가지 상태로 접속해, 페이지가 서버로 보낸 판정을 취합했다. 동일 IP(residential, Korea Telecom)·동일 세션 격리 조건. 재현 스크립트는 저장소의 test/compare-probe.mjs.
| 모드 | 설명 | 점수 | 판정 |
|---|---|---|---|
| baseline | headless, 입력 없음 (순수 자동화) | 90 | Bad Bot → Hard Challenge |
| activity | headless + 마우스/스크롤/키 입력 | 70 | 판단 보류 → Rate Limit |
| headful | 실제 창 + 입력 | 30 | Allow (통과) |
| stealth | JS 위장 + 입력 | 0 | Allow (완전 통과) |
IP/ASN·HTTP·TLS는 4개 모드 공통 0점(residential IP, 정상 헤더, TLS 미관찰)이라 생략.
| 신호 | 점수 | baseline | activity | headful | stealth |
|---|---|---|---|---|---|
navigator.webdriver === true | +30 | ✓ | ✓ | ✓ | · |
| plugins/mimeTypes 0개 | +10 | ✓ | ✓ | · | · |
| WebGL software renderer (SwiftShader) | +15 | ✓ | ✓ | · | · |
UA=Chrome인데 window.chrome 없음 | +15 | ✓ | ✓ | · | · |
| 수집 구간 내 행동 이벤트 0건 | +20 | ✓ | · | · | · |
| 합계 | 90 | 70 | 30 | 0 |
실측된 headless WebGL renderer 값: ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (LLVM 10.0.0) ...), SwiftShader driver) — GPU 없는 headless의 소프트웨어 렌더러가 그대로 노출됐다.
navigator.webdriver(+30)만으로는 allow 경계(≤30)를 못 넘는다. 실제 브라우저 자동화 + 약간의 상호작용이면 현재 스코어링을 통과한다.TLS ClientHello를 직접 종단해 JA3를 관찰하는 별도 엔드포인트(m2-tls-ja3-proxy, :8443)로 같은 실험을 반복한 결과. 실제 방문자 IP(residential) 조건.
| 클라이언트 | JA3 / GREASE | TLS 점수 | 총점 · 판정 |
|---|---|---|---|
| curl (Chrome UA 위조) | 375c6162… · GREASE 없음 | +25 | 50 · 판단 보류 |
| 실제 Chrome | GREASE 있음 | +0 | — |
| stealth headless | GREASE 있음(진짜 Chrome TLS) | +0 | 20 · Allow (통과) |
핵심: JA3는 UA만 Chrome으로 위조한 비브라우저 클라이언트(curl/Go/Python)를 +25로 적발한다 — JS를 아예 안 돌려 JS 레이어로는 못 잡던 사각지대를 덮는다. 하지만 stealth headless는 :8443에서도 그냥 통과(20점, Allow)한다. 진짜 Chromium이라 TLS도 진짜 Chrome(GREASE 있음) → JA3 +0, JS 신호는 stealth가 전부 가림. 즉 JA3는 JS 레이어를 보완할 뿐, 진짜 브라우저 자동화를 잡지는 못한다.
참고: stealth 세션을 같은 지문으로 연속 접속하면 남는 20점이 Behavior가 아니라 Cookie Continuity(반복 세션 재발급)로 잡히기도 한다 — stealth가 못 가리는 건 지문이 아니라 "반복 운영 흔적"이지만, 그마저도 Allow 구간(≤30)이다.
이 실험은 "JS 레이어 신호만으로는 고급 자동화를 못 막는다"는 결론을 숫자로 확인해준다(headful 30·stealth 0이 통과, JA3 추가해도 stealth headless는 20으로 통과). 실제 브라우저 자동화를 잡으려면 이 프로토타입이 관찰하지 않는 신호 — HTTP/2 프레임 지문, 장기 행동 궤적, 크로스 세션·크로스 고객사 클러스터, _abck식 암호화 연속 관찰 — 이 필요하며, 이는 알고리즘 정교함이 아니라 관측 레이어·규모의 문제다. (위 "Akamai급 격차" 섹션 참조.)
| 항목 | 한계 | 운영 배포 시 필요한 것 |
|---|---|---|
| TLS 지문 | 앱 서버 레이어라 관찰 불가 | Edge/Proxy(TLS 종단) 레이어에서 raw ClientHello 캡처 |
| IP 평판 | 무료 API, 레이트리밋·오프라인 시 실패 | 상용 IP Intelligence DB (MaxMind 등) |
| 세션/클러스터 저장소 | 프로세스 메모리, 재시작 시 초기화 | Redis/DB + TTL·보존 정책 |
| 행동 신호 | 2.5초 단발 관찰, 오탐 가능 | 세션 전체 기간 누적 관찰 |
| Account/Cart/Payment Flow | 미구현 | 비즈니스 로직과 연동 필요 |
| Challenge(JS/CAPTCHA) | 미구현 | 중간 위험 구간에 실제 챌린지 삽입 |
브라우저에서 실제로 관찰 가능한 신호를 수집해 Identification(이 방문자는 누구/무엇인가)과 Scoring(Good Bot인가 Bad Bot인가, 통과시킬 것인가 차단할 것인가)을 실시간으로 계산하는 프로토타입이다. 원본 근거는 docs/research/browser-identity-bot-management-signals.md.
| 탭 | 내용 |
|---|---|
| 분석 | 내 브라우저의 실시간 신호 수집 → Session/Cluster ID, risk score, 정책 결과 확인 |
| 기술지식 | 각 신호가 왜 유효한지, 이 프로토타입이 무엇을 관찰하고 무엇을 못 하는지 원리 설명 |
| 매뉴얼 | 사용 방법, 테스트 시나리오, API/점수/정책 레퍼런스 |
아래 순서대로 해보면 Identification과 Scoring 로직이 실제로 반응하는 걸 눈으로 확인할 수 있다.
| # | 시나리오 | 방법 | 기대 결과 |
|---|---|---|---|
| 1 | 정상 사용자 베이스라인 | 페이지를 열고 수집 구간(2.5초) 동안 마우스를 움직이거나 스크롤 | score 낮음 (Allow/Observe), Behavior 항목 0점 |
| 2 | 세션 리셋 vs 클러스터 유지 | "새 세션처럼 재분석" 버튼을 2~3번 연속 클릭 | Session ID는 매번 바뀌지만 Cluster ID는 고정. 반복될수록 Cookie Continuity 점수(+20)가 붙기 시작 |
| 3 | 단순 HTTP 클라이언트(비브라우저) 시뮬레이션 | 터미널에서 curl로 /api/analyze에 빈 JSON 바디 POST (README 예시 참고) | Client Hints 없음 + 헤더 개수 부족으로 HTTP Fingerprint 점수 상승 |
| 4 | 헤드리스 자동화 시뮬레이션 | Playwright/Puppeteer(headless: true, stealth 플러그인 미적용)로 이 페이지를 그대로 열기 | navigator.webdriver=true, WebGL이 SwiftShader/llvmpipe로 잡히며 score 급등 → Block |
| 5 | Good Bot 검증 | "기술지식 > Good Bot 검증" 탭 내용을 참고해 "분석" 탭 하단의 테스트 IP 입력창에 공개 Googlebot IP(예: 66.249.66.1) 입력 후 검증 | 역방향+정방향 DNS 매칭 성공 → 스코어링 생략, 즉시 Allow |
| 6 | 환경 불일치 시뮬레이션 | curl로 모바일 User-Agent 헤더를 보내되 payload의 touchSupport: false로 설정해 POST | "모바일 UA인데 터치 미지원" 항목에서 +15점 |
| 7 | 서버 재시작 후 클러스터 초기화 확인 | 서버를 껐다 켜고 동일 브라우저로 재접속 | 메모리 저장소 특성상 Cluster ID 자체는 동일하게 재계산되지만 이전 세션 이력은 사라짐 — "한계" 섹션에서 설명한 인메모리 저장소 특성 확인 |
| 엔드포인트 | 메서드 | 설명 |
|---|---|---|
/ | GET | 프로토타입 UI(이 페이지) 서빙 |
/api/analyze | POST | 클라이언트 지문 JSON을 받아 Session/Cluster ID, risk score, 정책을 계산해 반환. 쿼리 파라미터 ?testIp=로 IP 오버라이드(Good Bot 검증 데모용) 가능 |
/api/reset-session | POST | bm_sid 쿠키를 만료시켜 다음 분석 때 새 Session ID를 발급받게 함 |
| 구간 | 정책 |
|---|---|
| 0–30 | Allow |
| 31–55 | Observe (캐시 우선 + 모니터링) |
| 56–75 | Rate Limit + Selective Challenge |
| 76–90 | Origin Shield + Hard Challenge |
| 91+ | Block / Manual Review |
가중치 상세 표는 docs/bot-identification-scoring-model.md에 있다. 값은 코드(server.js의 scoreRequest)와 항상 동기화되어야 한다.