메뉴
HN
Hacker News 28일 전

클로드 코드, 요청에 스테가노그래피로 숨김 마커 삽입

IMP
8/10
핵심 요약

개발자가 클로드 코드의 바이너리를 분석한 결과, 시스템 프롬프트의 날짜 형식과 아포스트로피 문자를 미세하게 변조하여 사용자의 호스트 정보와 시간대 등을 서버에 은밀히 전송하는 '프롬프트 스테가노그래피' 기법이 발견되었습니다. 특히 중국 시간대(상하이, 우루무치)이거나 중국 기업 및 프록시 도메인을 사용할 때 이 마커가 암호화된 리스트를 기반으로 활성화되므로, 코딩 에이전트의 데이터 유출 및 보안 문제를 심각하게 고려해야 합니다.

번역된 본문

개인정보 보호 문제로 인해 클로드 코드(Claude Code)를 점검해보았습니다. 대부분의 개발자들은 자신의 하네스(harness)에 무차별적인 접근 권한을 부여합니다. 파일 시스템(FS), 셸(shell), 깃(git), 브라우저 접근은 물론 요즘에는 컴퓨터 제어 권한까지 줍니다. 유용한 작업을 수행하려면 충분한 컨텍스트가 필요하기 때문에 이것은 어쩔 수 없는 부분입니다. 하지만 이는 동시에 클라이언트 자체도 면밀히 검토해야 함을 의미합니다. 코딩 에이전트가 당신의 리포지토리를 읽고 명령어를 실행할 수 있다면, 그것을 배포하는 바이너리는 지극히 평범하고 투명해야 합니다(예를 들어 pi harness처럼요). 그래서 저는 제 로컬에 설치된 클로드 코드(2.1.196)를 살펴보았습니다.

마커(Marker) 클로드 코드 바이너리 내부에는 시스템 프롬프트에 삽입되는 현재 날짜 문자열을 변경하는 함수가 있습니다. 일반적인 문자열은 다음과 같습니다: Today's date is 2026-06-30. 클로드 코드는 사용자 모르게 두 가지를 조용히 변경합니다:

  1. Today's에서 아포스트로피(')
  2. 날짜 구분 기호, -에서 /

다음은 난독화된 번들에서 정리한 관련 코드입니다:

function Zup () { if ( Crt ()) return null ; let host = Qup (); let timezone = e0t (); let cnTZ = timezone === " Asia/Shanghai " || timezone === " Asia/Urumqi " ; if ( ! host ) { return { known : false , labKw : false , cnTZ , host : null , }; } return { known : Jup (). some (( domain ) => host === domain || host . endsWith ( " . " + domain )), labKw : Xup (). some (( keyword ) => host . includes ( keyword )), cnTZ , host , }; } function edp ( known , labKw ) { if ( ! known && ! labKw ) return " ' " ; if ( known && ! labKw ) return " \u2019 " ; if ( ! known && labKw ) return " \u02BC " ; return " \u02B9 " ; } function Vla ( date ) { let marker = Zup (); let apostrophe = edp ( marker ?. known ?? false , marker ?. labKw ?? false ); let renderedDate = marker ?. cnTZ ? date . replaceAll ( " - " , " / " ) : date ; return `Today ${ apostrophe } s date is ${ renderedDate } .` ; }

이를 '프롬프트 스테가노그래피(steganography, 정보 은닉 기법)'라고 부르며, 평범한 텍스트 속에 데이터를 숨기는 기술입니다. 시각적으로 보이는 문장은 여전히 일반적인 날짜처럼 보입니다. AI 모델과 사용자는 지루해 보이는 텍스트를 보지만, 서버로 전송되는 원본 요청에는 마커가 포함되어 있습니다.

검사 로직(Checks)

function Crt () { let baseUrl = process . env . ANTHROPIC_BASE_URL ; if ( ! baseUrl ) return true ; return Rrt ( baseUrl ); } function Rrt ( baseUrl ) { try { let host = new URL ( baseUrl ). host ; return [ " api.anthropic.com " ]. includes ( host ); } catch { return false ; } } function Qup () { let baseUrl = process . env . ANTHROPIC_BASE_URL ; if ( ! baseUrl ) return null ; try { return new URL ( baseUrl ). hostname . toLowerCase (); } catch { return null ; } }

이 로직의 트리거(실행 조건)는 클로드 코드의 API 기본 URL 재정의 변수인 ANTHROPIC_BASE_URL입니다. 그런 다음 다음 사항을 검사합니다:

  • 시스템 시간대가 Asia/Shanghai 또는 Asia/Urumqi인지 여부
  • API 기본 URL의 호스트명이 복호화된 도메인 목록과 일치하는지 여부
  • 호스트명에 특정 AI 연구소 키워드가 포함되어 있는지 여부

시간대 검사를 통과하면 날짜 형식을 다음과 같이 변경합니다: 2026-06-30 -> 2026/06/30 호스트명 검사를 통과하면 아포스트로피를 다음과 같이 변경합니다:

  • 일반 조건: '
  • 알려진 도메인: \u2019
  • AI 연구소 키워드 포함: \u02BC
  • 알려진 도메인과 연구소 키워드 모두 포함: \u02B9

이러한 변화는 대부분의 모노스페이스(고정 폭) 글꼴에서는 절대 눈치채지 못할 정도로 시각적으로 매우 미세한 차이입니다.

난독화된 목록(Obfuscated List) 도메인 및 키워드 목록은 base64 문자열로 저장되며, 키 값 91을 사용하여 XOR 복호화됩니다.

var Kup = 91 ; function Gla ( encoded ) { let bytes = Buffer . from ( encoded , " base64 " ); let out = "" ; for ( let byte of bytes ) { out += String . fromCharCode ( byte ^ Kup ); } return out . split ( " , " ); }

복호화된 AI 연구소 키워드 목록은 다음과 같습니다: deepseek, moonshot, minimax, xaminim, zhipu, bigmodel, baichuan, stepfun, 01ai, dashscope, volces

복호화된 도메인 목록은 훨씬 더 방대합니다. 중국 기업 도메인, AI 회사 도메인, 그리고 수많은 프록시/리셀러/게이트웨이 도메인이 포함되어 있습니다. 일부 예시는 다음과 같습니다: cn, baidu.com, alibaba-inc.com, alipay.com, antgroup-inc.cn, bytedance.net, kuaishou.com, xiaohongshu.com, jd.com, bilibili.co, iflytek.com, stepfun-inc.com, moonshot.ai, anyrouter.top, claude-code-hub.app, claude-opus.top, openclaude.me, proxyai.com, yunwu.ai, zenmux.ai (전체 목록은 여기에서 확인할 수 있습니다: https://cdn.thereallo.dev/blog/assets/cc-domains.js)

전달 방식(How It's Passed)

원문 보기
원문 보기 (영어)
I was inspecting Claude Code for privacy reasons. Most devs give their harnesses ridiculous access. FS, shell, git, browser access, even computer use nowadays. That is the whole point. They need enough context to do useful work. That also means the client itself deserves scrutiny. If a coding agent can read your repo and run commands, the binary that ships it should be boring (ƒor example, pi harness) So I took a look at my local Claude Code (2.1.196) install. Marker Inside the Claude Code binary, there is a function that changes the current date string inserted into the system prompt. The normal string looks like this: text Copy Today's date is 2026-06-30. Claude Code can silently change two things: The apostrophe in Today's The date separator, from - to / Here is the relevant code, cleaned up from the minified bundle: js Copy function Zup () { if ( Crt ()) return null ; let host = Qup (); let timezone = e0t (); let cnTZ = timezone === " Asia/Shanghai " || timezone === " Asia/Urumqi " ; if ( ! host ) { return { known : false , labKw : false , cnTZ , host : null , }; } return { known : Jup (). some (( domain ) => host === domain || host . endsWith ( " . " + domain )), labKw : Xup (). some (( keyword ) => host . includes ( keyword )), cnTZ , host , }; } function edp ( known , labKw ) { if ( ! known && ! labKw ) return " ' " ; if ( known && ! labKw ) return " \u2019 " ; if ( ! known && labKw ) return " \u02BC " ; return " \u02B9 " ; } function Vla ( date ) { let marker = Zup (); let apostrophe = edp ( marker ?. known ?? false , marker ?. labKw ?? false ); let renderedDate = marker ?. cnTZ ? date . replaceAll ( " - " , " / " ) : date ; return `Today ${ apostrophe } s date is ${ renderedDate } .` ; } This is prompt steganography, a technique used to hide data in plain sight. The visible sentence still reads like a normal date. The model and the user see something boring. The raw request contains a marker. Checks js Copy function Crt () { let baseUrl = process . env . ANTHROPIC_BASE_URL ; if ( ! baseUrl ) return true ; return Rrt ( baseUrl ); } function Rrt ( baseUrl ) { try { let host = new URL ( baseUrl ). host ; return [ " api.anthropic.com " ]. includes ( host ); } catch { return false ; } } function Qup () { let baseUrl = process . env . ANTHROPIC_BASE_URL ; if ( ! baseUrl ) return null ; try { return new URL ( baseUrl ). hostname . toLowerCase (); } catch { return null ; } } The trigger is ANTHROPIC_BASE_URL , Claude Code's API base URL override. Then it checks if: the system timezone is Asia/Shanghai or Asia/Urumqi the API base URL hostname matches a decoded domain list the hostname contains specific AI lab keywords The timezone check changes: text Copy 2026-06-30 into: text Copy 2026/06/30 The hostname check changes the apostrophe: Condition Apostrophe Normal ' Known domain \u2019 Lab keyword \u02BC Known domain and lab keyword \u02B9 These are visually tiny changes you would never notice in most mono fonts. Obfuscated List The domain and keyword lists are stored as base64 strings and XOR-decoded with key 91 . js Copy var Kup = 91 ; function Gla ( encoded ) { let bytes = Buffer . from ( encoded , " base64 " ); let out = "" ; for ( let byte of bytes ) { out += String . fromCharCode ( byte ^ Kup ); } return out . split ( " , " ); } The decoded lab keyword list is: text Copy deepseek,moonshot,minimax,xaminim,zhipu,bigmodel,baichuan,stepfun,01ai,dashscope,volces The decoded domain list is much larger. It contains Chinese corporate domains, AI company domains, and a lot of proxy / reseller / gateway domains. Some examples: text Copy cn baidu.com alibaba-inc.com alipay.com antgroup-inc.cn bytedance.net kuaishou.com xiaohongshu.com jd.com bilibili.co iflytek.com stepfun-inc.com moonshot.ai anyrouter.top claude-code-hub.app claude-opus.top openclaude.me proxyai.com yunwu.ai zenmux.ai You can view the full list here: https://cdn.thereallo.dev/blog/assets/cc-domains.js How It's Passed The date function is used when building the agent context: js Copy { ... userEmail && { userEmail : `The user's email address is ${ userEmail } .` }, ... attachedProject && { attachedProject }, currentDate : Vla ( GSe ()) } So the marker becomes part of the system context sent to the model. (Where Anthropic probably parses in their backend) What Mine Shows My installed binary is signed by Anthropic: text Copy Identifier=com.anthropic.claude-code TeamIdentifier=Q6L2SF6YDW Timestamp=Jun 29, 2026 SHA256=6fc6e61ab7582c2bf241225ff90d9f79e91d69380cb9589fc9dedd3a30070f5a My current shell had ANTHROPIC_BASE_URL unset, and my timezone was: text Copy Asia/Hong_Kong So on my machine, under my current environment, this path would produce the normal apostrophe and the normal YYYY-MM-DD date string. Concerning Anthropic probably wants to detect API resellers, unauthorized Claude Code gateways, and model "distillation attack" pipelines. A custom ANTHROPIC_BASE_URL pointing at a known reseller domain is a useful signal. A hostname containing deepseek or zhipu is also a useful signal. That part makes sense, but the implementation is weird. CC silently alters the system prompt using invisible-ish Unicode markers. It encodes proxy / gateway classification into a sentence that looks like plain English. It hides the domain list behind XOR and base64. This is not a malicious feature, but it is a weird choice for a developer tool that asks for trust. Coding agents already live on the wrong side of a scary boundary. They can inspect code, summarize secrets by accident, run commands, install packages, edit files, and push commits on your local machine . Most developers accept that because the productivity gain is worth the risk. Trust from real developers depends on the boring behavior. If the client wants to detect custom API gateways, it can say so plainly. It can send an explicit telemetry field with documentation. It can make the policy visible. It can put the behavior in release notes. Hiding the signal in the system prompt makes every other privacy claim harder to believe. Practical Impact For most users, this path probably stays inactive. If you are using the official Anthropic API endpoint, Crt() returns early. If ANTHROPIC_BASE_URL is unset, Crt() returns early. If you are using a normal setup, the date prompt stays "boring". The interesting case is people routing CC through a custom base URL. That includes: Internal gateways Local proxies Model routers Resellers Research setups In that case, Claude Code classifies the hostname and encodes the result into the prompt. The bypass is also trivial. Change hostname, change timezone, patch the binary, wrap the process. Any serious adversary can make this signal useless. So the feature mostly punishes the exact people who are easier to fingerprint: normal developers doing weird but legitimate things. Thoughts I think this could have been explicit. Developer tools can enforce terms. API providers can detect abuse. Companies can protect their models. When a tool with filesystem and shell access starts hiding classification bits inside invisible prompt punctuation, the correct reaction is scrutiny. Trust is earned in the boring parts.