ChatGPT가 Cloudflare Turnstile로 React 앱 상태까지 검사한다는 걸 암호 해독으로 밝혀냈다
ChatGPT Won't Let You Type Until Cloudflare Reads Your React State
TL;DR Highlight
ChatGPT가 메시지를 보내기 전에 Cloudflare Turnstile이 브라우저 지문은 물론 React 앱의 내부 상태(__reactRouterContext 등)까지 검사한다는 사실을, 암호화된 바이트코드를 직접 해독해 확인한 역공학 분석 글이다.
Who Should Read
봇 탐지/방지 시스템의 작동 원리가 궁금한 보안 개발자, 또는 자동화 도구나 스크래퍼를 만들면서 Cloudflare Turnstile의 내부 구조를 이해하고 싶은 개발자.
Core Mechanics
- ChatGPT는 메시지를 전송할 때마다 Cloudflare Turnstile 프로그램을 브라우저에서 조용히 실행한다. 이 프로그램은 요청마다 새로 암호화된 바이트코드(28,000자 base64)로 내려오며, 사용자는 이 검사가 끝나야 입력이 가능해진다.
- 암호화 방식은 두 겹이다. 외부 레이어는 prepare 요청의 p 토큰으로 XOR 복호화하고, 내부 19KB blob은 바이트코드 안에 float 리터럴(예: 97.35)로 박혀 있는 키로 XOR 복호화한다. 키가 페이로드 안에 포함되어 있어 HTTP 요청/응답만 있으면 완전히 복호화할 수 있다. 저자가 50개 요청에서 50번 모두 검증했다.
- 복호화된 프로그램은 커스텀 VM(28개 opcode)으로 작성되어 있으며, 레지스터 주소가 요청마다 랜덤화된다. 저자는 Cloudflare SDK 소스(sdk.js, 1,411줄)를 디컴파일해 opcode를 역추적했다.
- 프로그램이 수집하는 55개 속성은 세 레이어로 나뉜다. 1레이어는 브라우저 지문(WebGL 8개, 화면 8개, 하드웨어 5개, 폰트 측정 4개, DOM 8개, 스토리지 5개)이고, 2레이어는 Cloudflare 엣지 헤더(도시, 위도/경도, IP, 지역 등 5개)로 Cloudflare 네트워크를 통과한 요청에만 존재한다.
- 가장 주목할 만한 3레이어는 React 앱 상태 검사다. __reactRouterContext(React Router v6+가 DOM에 붙이는 내부 객체), loaderData(라우트 로더 결과), clientBootstrap(ChatGPT SSR 하이드레이션 전용 객체) 세 가지를 확인한다. 이 값들은 JavaScript 번들이 실제로 실행되고 React가 완전히 hydrate돼야만 존재한다.
- 이 때문에 브라우저 API만 흉내 내거나 HTML만 파싱하는 봇은 React 내부 상태를 재현할 수 없어 탐지된다. 이는 브라우저 레이어가 아닌 '애플리케이션 레이어'의 봇 탐지라고 저자는 설명한다.
- 수집된 지문은 localStorage에 키 '6f376b6560133c2c'로 저장되어 페이지 로드 간 지속된다. 최종적으로 55개 속성을 JSON으로 직렬화해 토큰을 생성하는 4개의 VM 명령으로 마무리된다.
- 377개의 Turnstile 프로그램 샘플을 분석했는데, 수집 속성 55개는 모든 샘플에서 변함없이 동일했다. 암호화 구조와 VM 레지스터 주소만 요청마다 달라진다.
Evidence
- OpenAI의 Integrity 팀 직원 Nick이 직접 댓글을 달았다. 이 검사는 봇, 스크래핑, 사기 등 플랫폼 남용을 막기 위한 것이며, 로그아웃 상태 무료 사용을 유지하기 위해 GPU 자원을 진짜 사용자에게 집중하려는 목적이라고 설명했다. 또한 페이지 로드 시간, 첫 토큰 시간, 페이로드 크기를 모니터링하며 대부분 사용자에게는 영향이 미미하고, 일부 소수만 약간의 지연을 겪는다고 밝혔다.
- 'React 앱이 완전히 hydrate돼야 속성이 존재한다'는 점이 봇 탐지의 핵심이라는 저자 주장에 대해, 'Headless 브라우저도 JavaScript를 실행하는데 이게 의미 있는 방어인가?'라는 반론이 있었다. 실제로 Chrome headless는 JS 번들을 실행하므로, 진짜 핵심은 '브라우저 API만 스텁으로 대체한 경량 봇 프레임워크'를 걸러내는 것에 가깝다는 의견도 나왔다.
- 완전한 Windows 11 VM에 Chrome과 GPU 가속을 켜서 실행하면 이 모든 검사를 우회할 수 있고, AWS 기준으로 페이지 로드 1,000회당 1센트 정도라 충분히 저렴하다는 의견이 있었다. 메모리 페이지 중복 제거 기술을 쓰면 동시에 50개 VM을 돌릴 수 있다는 구체적인 수치도 언급됐다.
- Cloudflare 관련 사용자 경험 불만도 제기됐다. Firefox 사용이나 '의심스럽다'고 판단된 IP 사용 시 캡챠를 과도하게 마주친다는 불만이 있었고, M5 MacBook Pro에서도 긴 대화 후 ChatGPT 탭이 멈추는 UX 문제를 겪는다는 사용자도 있었다.
- 이 글의 'So what?' 문제도 지적됐다. 결국 OpenAI가 자사 React 앱을 제대로 실행한 사용자만 받겠다는 건데, 그게 뭐가 문제냐는 반응이 여럿이었다. 역공학 자체는 흥미롭지만 '그래서 무료 API 추론이 가능해졌나요?'라는 댓글처럼, 실질적 결론이 불명확하다는 평이 많았다.
How to Apply
- 봇 탐지 시스템을 자체 서비스에 구축하려는 경우, React 앱 상태(__reactRouterContext, loaderData 같은 hydration 결과물)를 서버로 전송해 실제 SPA가 실행됐는지 검증하는 애플리케이션 레이어 검사를 추가하면, 브라우저 API만 흉내 낸 경량 봇을 효과적으로 걸러낼 수 있다.
- Cloudflare Turnstile을 도입하려는 경우, 이 분석을 통해 Turnstile이 단순 CAPTCHA가 아니라 55개 속성을 수집하는 VM 기반 지문 수집 시스템임을 이해하고, 사용자 지연 및 프라이버시 트레이드오프를 사전에 고려해 도입 여부를 결정할 수 있다.
- 자동화 테스트나 E2E 테스트 환경에서 ChatGPT나 Cloudflare Turnstile이 걸린 사이트를 테스트해야 한다면, API 스텁이나 경량 headless 모드 대신 실제 Chromium을 GPU 렌더링과 함께 실행해 React hydration까지 완료시켜야 Turnstile 검사를 통과할 수 있다.
- 보안 리서치 목적으로 Turnstile 바이트코드를 분석하려면, 저자가 공개한 5단계 복호화 체인(p 토큰 XOR → float 키 추출 → 내부 blob 복호화)을 참고해 HTTP 트래픽 캡처만으로 프로그램 전체를 복호화할 수 있다.
Code Example
snippet
# 저자가 공개한 Turnstile 외부 레이어 복호화 코드
import base64, json
# Step 1: prepare 요청에서 p_token, 응답에서 dx를 읽어온다
# Step 2: XOR 복호화로 외부 바이트코드(89개 VM 명령) 추출
outer = json.loads(bytes(
base64.b64decode(dx)[i] ^ p_token[i % len(p_token)]
for i in range(len(base64.b64decode(dx)))
))
# → 89개 VM 명령 배열
# Step 3: 내부 19KB blob 이후 5-arg 명령어를 찾아 마지막 인수(float)를 XOR 키로 사용
# 예: [41.02, 0.3, 22.58, 12.96, 97.35] → 키는 97.35
# Step 4: str(key)로 내부 blob XOR 복호화 → 417~580개 VM 명령 포함 JSON
inner = json.loads(bytes(
base64.b64decode(blob)[i] ^ ord(str(key)[i % len(str(key))])
for i in range(len(base64.b64decode(blob)))
))
# Turnstile이 확인하는 React 앱 상태 속성 (Layer 3)
# - window.__reactRouterContext (React Router v6+ DOM 내부 객체)
# - loaderData (라우트 loader 결과)
# - clientBootstrap (ChatGPT SSR hydration 전용)
# localStorage에 지문이 저장되는 키
FINGERPRINT_KEY = '6f376b6560133c2c'Terminology
TurnstileCloudflare가 제공하는 봇 탐지 솔루션. 사용자에게 CAPTCHA를 보여주는 대신 브라우저에서 JavaScript 프로그램을 몰래 실행해 사람인지 봇인지 판별한다.
hydration서버에서 HTML을 미리 렌더링(SSR)한 후, 클라이언트에서 JavaScript가 실행되면서 그 HTML에 이벤트 핸들러와 상태를 '다시 붙이는' 과정. 이 과정이 완료돼야 React 앱이 온전히 동작한다.
XOR 암호화두 데이터를 비트 단위로 배타적 논리합 연산하는 단순 암호화. 같은 키로 두 번 XOR하면 원본이 복원된다.
VM bytecode가상 머신(Virtual Machine)이 해석하는 명령어 집합. 여기서는 Cloudflare가 직접 정의한 28개 opcode를 쓰는 커스텀 VM으로, 역공학을 어렵게 하기 위해 사용된다.
WebGL fingerprintingGPU의 제조사, 드라이버, 렌더러 정보를 WebGL API로 읽어 기기를 식별하는 기법. 같은 기기라도 GPU 조합이 독특해 높은 식별력을 가진다.
SSR (Server-Side Rendering)서버에서 HTML을 미리 만들어 클라이언트에 전송하는 방식. SEO와 초기 로딩 속도에 유리하지만, 이후 클라이언트에서 hydration이 필요하다.