M2 Bot Identification & Scoring — 프로토타입

이 브라우저에서 실제로 관찰 가능한 신호(쿠키, 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 지문(JA3/JA4) — 왜 이 프로토타입은 관찰하지 않는가

TLS 지문은 클라이언트가 서버에 보내는 ClientHello 메시지의 필드(TLS 버전, 암호화 스위트 순서, 확장 목록, 타원 곡선)를 해시한 값이다. HTTP 레이어가 아니라 TLS 핸드셰이크 레이어에서 결정되므로 User-Agent를 바꿔도 우회되지 않는다.

JA3 = MD5( TLS버전 + 암호화스위트 + 확장목록 + 타원곡선 + 포인트형식 )

Chrome: 고유 JA3 해시
Node.js fetch / curl / axios: Chrome과 다른 JA3 → 즉시 식별 가능

문제는 이 값이 raw ClientHello 바이트를 TLS 협상이 끝나기 전에 캡처해야 나온다는 점이다. Node.js의 http/https 모듈은 이미 협상이 끝난 소켓만 애플리케이션 코드에 넘겨주므로, 이 프로토타입처럼 앱 서버 레벨에서 동작하는 한 JA3를 관찰할 방법이 없다. 실제로 이걸 관찰하려면 nginx/Envoy/CDN Edge처럼 TLS를 직접 종단하는 레이어가 필요하다 — 원본 리서치 문서가 "TLS/HTTP fingerprint"를 별도 섹션으로 다루는 이유이자, M2가 이 기능을 앱이 아니라 Edge/Proxy에 둬야 하는 이유다.

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 등)매우 낮음해당 벤더 뒤에 있다면 재구현 불필요, 벤더가 계산한 값을 헤더로 전달받기만 하면 됨

Go로 구현할 때도 정밀도 2단계

방식난이도얻는 것한계
표준 라이브러리 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는 어렵지 않게 흉내낼 수 있다

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" 섹션과 같은 결론이다.

🖥️Browser Fingerprint — 어떤 값이 왜 점수에 반영되는가
신호정상 브라우저자동화/헤드리스
navigator.webdriverfalse 또는 속성 없음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 hashGPU/오디오 스택별 미세한 렌더링 차이 존재동일 headless 이미지에서 계속 같은 해시 반복 → 클러스터로 묶임

puppeteer-extra-plugin-stealth 같은 도구는 navigator.webdriver를 숨기고 일부 속성을 패치할 수 있지만, WebGL이 실제로 소프트웨어 렌더러를 쓰고 있다는 사실 자체는 GPU가 없는 한 바꿀 수 없다. 그래서 이 프로토타입은 여러 지표를 동시에 봐서 하나를 속여도 다른 지표에서 걸리게 설계했다.

🍪Session ID vs Cluster ID — 왜 두 개로 나눴는가

쿠키 하나만 ID로 쓰면, 봇 운영자가 차단당할 때마다 쿠키(또는 프로필)를 새로 만들어 우회할 수 있다. 원본 문서 9장(Long-Term Cluster)의 핵심도 "IP/쿠키 단위가 아니라 fingerprint cluster 단위로 정책을 적용하라"는 것이다.

Session ID (bm_sid 쿠키) ── 초기화 가능, 짧은 수명
Cluster ID (지문 해시) ── 브라우저/디바이스가 그대로면 쿠키를 지워도 동일

서버는 Cluster ID → 발급된 Session ID 목록을 추적
같은 Cluster에서 Session이 반복 재발급 → reset 패턴 의심 (+20점)

이 프로토타입에서는 메모리 Map으로 구현했지만, 실제 서비스에서는 Redis 등 TTL이 있는 저장소가 필요하다.

Good Bot 검증 원리 — 역방향 DNS + 정방향 재검증

Googlebot/Bingbot 같은 "알려진 좋은 봇"은 User-Agent가 아니라 네트워크 신원으로 검증해야 한다. UA 문자열은 누구나 복사할 수 있기 때문이다. Google/Bing이 공식적으로 권장하는 방법은 다음과 같다.

1. 요청 IP → 역방향 DNS 조회 (dns.reverse)
2. 결과 hostname이 googlebot.com / search.msn.com 등으로 끝나는지 확인
3. 그 hostname을 다시 정방향 DNS 조회 (dns.lookup)
4. 정방향 조회 결과 IP === 원래 요청 IP 인지 재확인
   두 단계 모두 통과해야 검증 성공 (Allow, 스코어링 생략)

1번만 확인하면 위조된 hostname을 스스로 등록한 공격자에게 속을 수 있어 4번 정방향 재검증이 필수다. Akamai/Cloudflare도 이런 검증을 거친 크롤러만 IP 화이트리스트에 등록해 "Verified Bot"으로 분류하고 TLS/JS 검사를 생략시킨다.

🏛️Akamai급 상용 솔루션과의 격차 — 이 정도 기술로 봇을 막을 수 있는가

지금까지 다룬 IP평판/TLS/HTTP/브라우저지문/쿠키 5개 카테고리 외에 Akamai Bot Manager 같은 상용 솔루션이 구조적으로 더 갖는 것들이 있다.

요소내용이 프로토타입과 차이
_abck 쿠키JS 센서가 세션 내내 계속 마우스 궤적 곡률/가속도, 이벤트 디스패치 타이밍 jitter, 모바일 자이로/가속도계까지 수집해 암호화 → 매 요청마다 서버가 복호화 검증이 프로토타입은 페이지 로드 시 2.5초 스냅샷 1회뿐. Akamai는 누적 신뢰도, 우리는 단발 판정
폴리모픽 난독화센서 수집 JS 코드 구조를 주기적으로 변경 — 우회법이 공개돼도 다음 난독화 사이클에서 무력화우리 스코어링 로직은 고정 코드 + 공개 가중치
크로스 고객사 네트워크 효과수만 개 고객사 트래픽을 동시에 관측 — 봇넷이 A사 공격 후 인프라를 재사용해 B사를 공격하면 즉시 탐지단일 회사가 자기 로그만 보고는 절대 얻을 수 없는 관측 규모. 알고리즘 격차가 아니라 데이터 규모 격차
실시간 대응 루프공개된 우회 기법이 퍼지면 수일 내로 타겟 대응(난독화 갱신, 우회 도구 시그니처 탐지) 배포정적 룰셋이 아니라 지속적인 공격-방어 사이클
네이티브 앱 SDKiOS/Android 앱에 내장 — 탈옥/루팅, Frida/Xposed 후킹, 에뮬레이터 탐지브라우저 기반 지문 수집으로는 아예 손댈 수 없는 영역

그래서 막을 수 있는가

하위~중급 자동화는 막힌다. 단순 스크립트, curl/requests 기반 스크레이퍼, 세션 관리 없는 크리덴셜 스터핑 — 지금 프로토타입 수준의 다중 카테고리 합산 스코어링으로도 충분히 걸러진다. 원본 문서가 처음부터 "완벽한 식별"이 아니라 "저품질 자동화를 낮은 비용으로 거른다"를 목표로 잡은 이유다.

고급/타겟형 자동화는 막히지 않는다. 실제 Chrome + stealth plugin + residential proxy + 사람처럼 짠 행동 스크립트 조합이면, 알고리즘을 아무리 정교하게 짜도 단일 조직 구현으로는 못 막는다. 여기서 진짜 격차는 "알고리즘이 더 똑똑해서"가 아니라 위 표의 크로스 고객사 데이터 — 순수한 관측 규모의 문제다. 그래서 목표를 "차단"이 아니라 "이 타겟을 공격할 가치가 없을 만큼 운영 비용을 올린다"로 잡는 게 현실적이다.

🔐_abck 쿠키 메커니즘 상세

Akamai가 공식 스펙을 공개하지 않아 아래 내용은 보안 리서치 커뮤니티가 리버스엔지니어링으로 관찰·정리한 내용이다. 정확한 암호화 알고리즘·포맷은 Akamai가 주기적으로 바꾸므로 시점에 따라 달라질 수 있다.

전체 흐름

1. 최초 요청
  클라이언트 → 보호된 페이지 요청
  Akamai Edge → HTML 응답에 난독화된 센서 스크립트 <script> 태그 삽입
  (경로/해시가 계속 바뀜 — 예: /akam/13/... 형태)

2. 센서 스크립트 실행 (브라우저 안에서)
  - 환경 지문: navigator 속성, Canvas/WebGL, 폰트 목록, 화면 크기, timezone 등
  - 자동화 흔적: navigator.webdriver, ChromeDriver가 주입하는 window.cdc_* 전역 변수
  - 연속 행동 스트림: 마우스 좌표+타임스탬프(속도·가속도·곡률), 스크롤, 키 입력 간격, 터치/자이로
  - 이벤트 디스패치 타이밍의 미세한 jitter (합성하기 어려운 신호)

3. 인코딩 + 암호화
  수집 데이터를 자체 포맷(sensor_data)으로 직렬화 후 암호화/난독화
  → 동적으로 이름이 바뀌는 엔드포인트로 POST 전송

4. Edge에서 검증 + 스코어링
  sensor_data 복호화 → 룰 기반 + ML 스코어링 → _abck 쿠키 발급/갱신
  (클라이언트는 복호화 키가 없어 내용을 못 읽고 위조도 못 함)

5. 이후 모든 요청
  요청마다 _abck 쿠키 포함 필수 → Edge가 서버 키로 복호화 → 내부 플래그 검증
  - trust 윈도우가 아직 유효한가 (보통 분~시간 단위로 짧게 재검증)
  - 새 sensor_data가 계속 들어와서 trust를 갱신하고 있는가
  - 이 요청의 IP/UA가 토큰 발급 시점 맥락과 크게 어긋나지 않는가

6. sensor_data 없이/무효하게 요청이 오면
  즉시 차단 대신 → JS 챌린지 재제시 (센서 페이지를 다시 로드시켜 기회를 줌)
  반복 회피/실패 시 → 429/403 하드 차단

쿠키 값 구조 (커뮤니티 관찰 기준, 비공식)

값 자체는 사람이 읽을 수 없는 불투명한 토큰이지만, 커뮤니티 리서치에서는 ~로 구분된 여러 세그먼트(세션 식별자 성격의 해시, 타임스탬프, 상태 플래그)로 구성되고, 마지막 세그먼트(예: ~-1~, ~0~, ~1~)가 "이 세션이 봇으로 플래그됐는지"를 유추하는 경험적 신호로 관찰돼왔다. Akamai가 공식 문서화한 스펙이 아니므로 언제든 바뀔 수 있다는 점을 감안해야 한다.

왜 단순 쿠키 위조로 안 뚫리는가

  • 클라이언트는 암호화 키가 없어 값을 직접 조작할 수 없음
  • 리플레이 방지: 이전 유효 토큰/sensor_data를 재사용해도 시간이 지나면 키·포맷이 바뀌어 무효화되거나 서버가 중복 패턴을 탐지
  • 센서 스크립트 자체에 안티디버깅/안티변조 체크가 있어 devtools가 열려있거나 스크립트가 수정된 걸 감지하면 동작을 바꾸거나 플래그를 남김

이 프로토타입과의 실질적 차이

우리 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 바디)클라이언트위조 가능

구멍 1 — X-Forwarded-For 무검증 신뢰

X-Forwarded-For는 클라이언트가 임의로 넣을 수 있는 헤더다. 이 값을 클라이언트 IP로 그대로 쓰면 두 가지 우회가 동시에 열린다.

공격 A) X-Forwarded-For: 66.249.66.1 (Googlebot 공개 IP)
  → 서버가 이 IP로 역방향 DNS 검증 → 통과 → Good Bot Allow (판정 우회)

공격 B) X-Forwarded-For: 127.0.0.1 + ?testIp=<크롤러IP>
  → "로컬이니 testIp 허용" 게이트가 열림 → 동일하게 Allow 우회

방어: 클라이언트 IP의 진실은 위조 불가능한 소켓 peer(req.socket.remoteAddress)뿐이다. XFF는 기본적으로 무시하고, 실제 신뢰 가능한 프록시 뒤에 배포할 때만 TRUST_XFF=1 환경변수로 활성화한다. 이때도 프록시가 XFF를 덮어써야(append가 아니라 replace) 클라이언트가 앞에 위조 값을 끼워넣는 걸 막을 수 있다.

구멍 2 — testIp 데모 파라미터의 verdict 우회

로컬에서 크롤러 검증을 시연하려고 연 ?testIp= 오버라이드가, 아무 가드가 없으면 원격 스크레이퍼가 ?testIp=<크롤러IP>만 붙여 Allow를 얻는 통로가 된다.

방어: testIp는 실제 소켓 peer가 로컬/사설망일 때(=개발자 본인)만 적용한다(XFF가 아니라 소켓 peer로 판정). 응답의 request.testIpApplied로 적용 여부가 노출되고, UI에도 "testIp 오버라이드(로컬 데모)" 배지가 표시된다. 운영에서는 파라미터 자체를 제거하는 게 안전하다.

구멍 3 — 대시보드 XSS (공격자 UA가 화면에서 실행)

이 도구는 적대적 트래픽을 들여다보는 화면이다. 봇이 User-Agent나 WebGL renderer 값에 <img src=x onerror=...> 같은 페이로드를 넣고, 운영자가 그 세션을 대시보드에서 열면 운영자 브라우저에서 스크립트가 실행된다. 관측 대상의 값이 곧 공격 벡터가 되는 구조다.

방어: 서버가 돌려준 모든 클라이언트/외부 유래 문자열을 innerHTML에 넣기 전에 HTML 이스케이프(esc())한다. Session/Cluster ID처럼 서버가 생성한 값은 textContent로 출력한다.

구멍 4 — 자기신고 값에 대한 과신

POST 바디의 행동 카운트(mousemove 등)나 navigator 값은 봇이 얼마든지 지어낼 수 있다. 그래서 서버가 독립적으로 검증 가능한 신호(소켓 IP, 헤더 존재/순서, DNS)의 가중치를 자기신고 신호보다 구조적으로 높게 둬야 한다. 이 프로토타입도 webdriver/WebGL 같은 자기신고 신호 하나만으로 차단하지 않고 여러 카테고리를 합산하는 이유가 여기 있다.

부수적 하드닝

  • 요청 바디 1MB 초과 시 스트림을 계속 받지 않고 413 Payload Too Large 반환 — 메모리 고갈 방지.
  • 지문 신호가 하나도 없는 요청(빈 바디)은 Cluster ID를 null로 두어, 신호 없는 클라이언트끼리 한 클러스터로 뭉쳐 "reset 패턴" 오탐이 나는 것을 방지.
🧪실측 실험 — headless Chrome은 실제로 잡히는가

실제 Playwright(Chromium 149)로 이 배포 페이지에 4가지 상태로 접속해, 페이지가 서버로 보낸 판정을 취합했다. 동일 IP(residential, Korea Telecom)·동일 세션 격리 조건. 재현 스크립트는 저장소의 test/compare-probe.mjs.

결과 요약

모드설명점수판정
baselineheadless, 입력 없음 (순수 자동화)90Bad Bot → Hard Challenge
activityheadless + 마우스/스크롤/키 입력70판단 보류 → Rate Limit
headful실제 창 + 입력30Allow (통과)
stealthJS 위장 + 입력0Allow (완전 통과)

신호별 발화 매트릭스

IP/ASN·HTTP·TLS는 4개 모드 공통 0점(residential IP, 정상 헤더, TLS 미관찰)이라 생략.

신호점수baselineactivityheadfulstealth
navigator.webdriver === true+30·
plugins/mimeTypes 0개+10··
WebGL software renderer (SwiftShader)+15··
UA=Chrome인데 window.chrome 없음+15··
수집 구간 내 행동 이벤트 0건+20···
합계9070300

실측된 headless WebGL renderer 값: ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (LLVM 10.0.0) ...), SwiftShader driver) — GPU 없는 headless의 소프트웨어 렌더러가 그대로 노출됐다.

핵심 발견

  • 순수 headless는 확실히 잡힌다(90). webdriver + software GPU + window.chrome 부재 + 행동 부재가 겹쳐 challenge 구간.
  • 행동 하나 위조로 −20(70). 마우스/스크롤/키 입력만 넣어도 "행동 부재"가 사라진다 — 행동 신호를 단독 근거로 쓰면 안 되는 이유.
  • ⚠️ 실제 창(headful)은 webdriver 하나만 남아 30점 = Allow로 통과. 실제 Chrome 자동화는 GPU·plugins·window.chrome이 전부 정상이라, 남는 navigator.webdriver(+30)만으로는 allow 경계(≤30)를 못 넘는다. 실제 브라우저 자동화 + 약간의 상호작용이면 현재 스코어링을 통과한다.
  • stealth는 0점 — 완전 통과. JS 레이어를 위장하면, 이 앱은 TLS/HTTP2를 관찰하지 않고 IP도 깨끗한 residential이라 남는 신호가 없다.

TLS/JA3 프록시(:8443)에서 다시 측정하면?

TLS ClientHello를 직접 종단해 JA3를 관찰하는 별도 엔드포인트(m2-tls-ja3-proxy, :8443)로 같은 실험을 반복한 결과. 실제 방문자 IP(residential) 조건.

클라이언트JA3 / GREASETLS 점수총점 · 판정
curl (Chrome UA 위조)375c6162… · GREASE 없음+2550 · 판단 보류
실제 ChromeGREASE 있음+0
stealth headlessGREASE 있음(진짜 Chrome TLS)+020 · 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
5Good 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 자체는 동일하게 재계산되지만 이전 세션 이력은 사라짐 — "한계" 섹션에서 설명한 인메모리 저장소 특성 확인
🔗API 레퍼런스
엔드포인트메서드설명
/GET프로토타입 UI(이 페이지) 서빙
/api/analyzePOST클라이언트 지문 JSON을 받아 Session/Cluster ID, risk score, 정책을 계산해 반환. 쿼리 파라미터 ?testIp=로 IP 오버라이드(Good Bot 검증 데모용) 가능
/api/reset-sessionPOSTbm_sid 쿠키를 만료시켜 다음 분석 때 새 Session ID를 발급받게 함

curl 예시

curl -X POST http://localhost:8787/api/analyze \
  -H "Content-Type: application/json" \
  -d '{}'
📊점수·정책 요약 레퍼런스
구간정책
0–30Allow
31–55Observe (캐시 우선 + 모니터링)
56–75Rate Limit + Selective Challenge
76–90Origin Shield + Hard Challenge
91+Block / Manual Review

가중치 상세 표는 docs/bot-identification-scoring-model.md에 있다. 값은 코드(server.jsscoreRequest)와 항상 동기화되어야 한다.