AI 서비스에서도 격리가 무너질 수 있다: Workers AI 데이터 혼입 사례를 통해 전송 데이터와 실행 위치 분리하기
요약
Cloudflare Workers AI에서 GLM-5.3 모델 사용 중 다른 이용자의 요청 내용이 혼입되는 보안 결함이 발생했습니다. 이는 KV 캐시 할당량과 컨텍스트 길이의 불일치 및 SGLang 추론 엔진의 메모리 초기화 실패가 복합적으로 작용한 결과입니다. 특히 클라이언트에게 HTTP 200(성공) 코드가 반환되어 데이터 유출 사실을 인지하기 어렵게 만들었습니다.
핵심 포인트
- KV 캐시와 컨텍스트 길이의 불일치가 결함 발생의 주요 원인이었습니다.
- SGLang 엔진이 거부된 요청을 제대로 처리하지 못하고 이전 메모리를 읽어 데이터를 유출했습니다.
- HTTP 200 성공 응답으로 인해 데이터 혼입 사실이 은폐될 위험성이 있습니다.
- 개발자는 전송 데이터 마스킹 및 반환 데이터 검증 로직 구현이 필수적입니다.
서론
2026년 10월 26일, Cloudflare가 Workers AI(Cloudflare 서버에서 AI 모델을 구동하는 서비스)의 결함을 공표했습니다. 내용은 긴 프롬프트에 대한 답변 과정에서 다른 이용자의 요청 내용이 혼합되는 현상이 있었다는 것입니다.
본 기사에서는 먼저 공식 발표에 명시된 내용을 정리합니다. 그 후, 인프라 담당자로서 무엇을 가져가야 할지, 그리고 직접 Docker 환경에서 '전송하기 전에 마스킹(masking)하는 것'과 '돌아온 데이터를 확인하는 것'을 실제로 구현해 본 결과를 소개하겠습니다.
출처는 모두 Cloudflare 공식 블로그입니다.
무엇이 발생했나 (공식 발표 수치만)
공식 블로그에 기록된 사실만을 요약했습니다. 추측은 포함하지 않았습니다.
| 항목 | 내용 |
|---|---|
| 대상 모델 | GLM-5.3 (@cf/zai-org/glm-5.3). 다른 모델은 영향 없음으로 확인됨 |
| ... | |
| 시계열 정보는 공식적으로 UTC(협정 세계시)를 사용합니다. 한국 시간(JST)으로 변환하면 다음과 같습니다. |
- 9월 9일 08:11 JST: HackerOne을 통해 보고 접수 (UTC 9월 8일 23:11)
- 9월 9일 22:22 JST: 보안 사고 선언. 별개의 두 계정으로 25분 이내에 재현됨
- 9월 9일 23:00 JST: 모델을 비공개 처리하고, 요청은 HTTP 400을 반환하도록 설정
- 9월 10일 04:40 JST: 수정된 구성으로 모델 재공개 (UTC 9월 9일 19:40)
왜 발생했나 (공식 설명 범위 내)
공식은 '두 가지 문제가 중첩되었다'고 설명했습니다.
첫 번째 문제: 설정의 불일치
GLM-5.3는 1,048,576 토큰의 컨텍스트(한 번에 읽을 수 있는 프롬프트 길이)로 공개되어 있었습니다. 그런데 프롬프트를 처리하는 GPU 측의 KV 캐시(모델이 계산 중간 결과를 저장해 두는 메모리 영역)는 681,280 토큰분밖에 할당되지 않았습니다.
Cloudflare는 이 불일치를 공개 전에 알고 있었습니다. 하지만 '들어가지 않는 프롬프트는 엔진이 에러를 반환할 것'이라고 생각했고, 긴 프롬프트 테스트에서도 성공 응답이 돌아왔기 때문에 문제가 없다고 판단했던 것입니다.
두 번째 문제: SGLang의 결함
SGLang(Cloudflare가 사용하는 오픈소스 추론 엔진)은 프롬프트 처리(prefill)와 답변 생성(decode)을 별도의 GPU로 분담하는 '분리 구성'으로 작동하고 있었습니다.
들어가지 않는 프롬프트는 내부적으로 '거부' 표시가 붙고, 1 토큰의 임시 내용으로 대체됩니다. 원래라면 여기서 에러를 반환했어야 하지만, 분리 구성에서는 '완료 여부'만 확인했을 뿐, 거부되었다는 사실이 decode 측에 전달되지 않았습니다.
그 결과, decode 측은 '긴 프롬프트분이 작성되었다'고 착각하고, 이전 요청이 남긴 메모리를 읽어 답변을 생성했습니다. 공식에 따르면, KV 캐시 페이지는 성능상의 이유로 요청마다 0으로 초기화되지 않았다고 합니다.
가장 무서운 점은 'HTTP 200'으로 반환되었다는 것
공식 설명에서 가장 심각한 부분이라고 생각합니다.
The client got HTTP 200 instead of HTTP 400. (클라이언트는 HTTP 400 대신 HTTP 200을 받았습니다.)
원래는 400(요청 오류)이 되어야 했지만, 200(성공)으로, 게다가 문장으로서 자연스러운 답변이 반환되었습니다. Cloudflare 자신도 '테스트에서는 HTTP 상태 코드와 성능만 확인했을 뿐, 출력 내용까지는 확인하지 못했다'고 언급했습니다.
대응 방안
공식적으로 발표된 주요 조치들입니다.
- KV 캐시를 약 204만 토큰분으로 확장하여 컨텍스트보다 크게 설정함.
- 거부된 요청을 전달하기 전에 중단하도록 엔진을 수정함.
- SGLang을 분리 구성으로 사용하는 사업자에게는
v0.5.22 이상 버전으로 업데이트할 것을 권고하고 있습니다.
인프라 담당자로서의 교훈
여기부터는 필자의 고찰입니다.
교훈 1: HTTP 200이라도, 내용은 반드시 확인해야 한다
모니터링이나 테스트가 '상태 코드가 200인지'로만 끝나지 않는지 재점검하는 계기가 됩니다. AI의 응답은 형식적으로는 올바르더라도 내용이 정확하지 않을 수 있습니다. 이번 사례는 그야말로 '200 상태 코드에 자연스러운 문장이지만, 타인의 데이터'였습니다.
자신의 요청에 표식(캐나리아 마커, 실제 데이터에 섞어 놓는 구별용 문자열)을 넣어, 응답에서 자신의 표식만 있는지 확인하는 방법도 현지에서도 할 수 있습니다. Cloudflare 역시 수정 확인에 canary-marker(표식) 테스트를 사용했다고 언급하고 있습니다.
교훈2: 기밀 정보는 외부에 보내지 않는다. 보낸다면 비식별화해야 한다
아무리 큰 사업자라도 서비스 내부의 격리(테넌트 분리)가 무너질 수 있습니다. 사용자 측에서 할 수 있는 것은 애초에 섞여서 문제가 될 만한 데이터를 보내지 않는 것입니다.
- 개인정보나 API 키는 전송하기 전에 비식별화 처리한다 (마스킹)
- 비식별화 처리 자체는 외부와 연결되지 않은 환경에서 실행한다
교훈3: '상한선은 스스로 측정해야 한다'
공식 운영자용 권장 사항 중 하나는 'KV 풀의 크기와 컨텍스트 길이를 엔진의 시작 로그로부터 측정하여 비교할 것. 엔진이 거부해 줄 것이라고 맹신하지 말 것'이었습니다. 직접 SGLang 등을 구동하는 경우라면 그대로 가져갈 수 있는 교훈입니다.
현지에서 가능한 분리: Docker로 시도한 최소 예시
여기부터는 필자가 실제로 구동한 결과를 바탕으로 설명합니다.
시도 환경
- Linux (x86_64), Docker Engine 29.9.1
- 이미지:
alpine:3.20
(내용물은 3.20.10) - 실행일: 2026년 10월 27일
로컬 LLM 자체는 무겁기 때문에, 이번에는 **'전송 전 비식별화 컨테이너'**와 '돌아온 답변 검사 컨테이너' 두 가지만 만들었습니다. 둘 다 외부 네트워크에 연결되지 않도록 설정하고 구동합니다.
단계 1: 비식별화 스크립트
mask.sh
을 만듭니다. 이메일 주소, API 키로 보이는 문자열, 전화번호를 비식별화 처리합니다.
#!/bin/sh
# 표준 입력의 기밀성 문자를 비식별화하여 표준 출력으로 보냄
sed -E \
...
시도할 입력 파일은 sample.txt입니다. 키는 실제가 아닌 더미 문자열입니다.
문의: [email protected] / 090-1234-5678
설정: XAI_API_KEY=xai-AAAABBBBCCCCDDDDEEEE1234
본문: 로그인할 수 없으니 봐주세요
단계 2: 네트워크가 없는 컨테이너에서 비식별화 처리
--network none
(네트워크를 연결하지 않음)과 --read-only
(파일을 수정할 수 없음) 옵션을 붙여서 구동합니다.
chmod +x mask.sh
docker run --rm -i --network none --read-only \
-v "$PWD/mask.sh:/mask.sh:ro" alpine:3.20 /mask.sh < sample.txt
결과입니다.
문의: [EMAIL] / [PHONE]
설정: XAI_API_KEY=[API_KEY]
본문: 로그인할 수 없으니 봐주세요
키 이름(XAI_API_KEY)
은 남아있고, 값만 비식별화되었습니다. 외부에 전송하는 것은 이 비식별화된 텍스트만 합니다.
단계 3: 정말 외부와 연결되지 않는지 확인하기
docker run --rm --network none alpine:3.20 wget -q -T 3 -O- https://example.com
echo "exit=$?"
docker run --rm --network none alpine:3.20 ip -o link
결과입니다.
wget: bad address 'example.com'
exit=1
1: lo: ...
도메인 이름에서 IP 주소를 알아내는 이름 해석(Name Resolution)조차 할 수 없었으며, 사용할 수 있는 네트워크는 lo
(자신과의 통신용)뿐이었습니다. 비식별화 전의 원본 데이터를 가진 컨테이너가 외부로 무언가를 보낼 경로가 없습니다.
또한 --read-only
로 쓰기 작업도 불가능함을 확인했습니다.
docker run --rm --read-only alpine:3.20 touch /tmp/x
touch: /tmp/x: Read-only file system
단계 4: 돌아온 답변을 표식으로 검사하기
교훈1의 '200이라도 내용을 확인하는 것'을 최소한의 형태로 만들었습니다. check.sh는 자신의 표식이 있고, 다른 표식(CANARY-
)"로 시작하는 다른 문자열이 없을 때만 OK 처리합니다.
#!/bin/sh
# 사용법: check.sh 자신의 카나리아 < 응답 파일
# 응답에 '자신의 표시'가 있고, '다른 표시(CANARY-*)'가 없으면 OK
...
정상적인 응답(ok.txt)과 다른 표시가 섞인 응답(mixed.txt)을 준비했습니다. 둘 다 수동으로 만든 예시입니다.
요약: 로그인 장애 상담 건입니다. (CANARY-A1B2)
요약: 로그인 장애 상담 건입니다. (CANARY-A1B2)
별건: 청구서 관련 문의 (CANARY-Z9Y8)
chmod +x check.sh
for f in ok.txt mixed.txt; do
docker run --rm -i --network none --read-only \
...
결과입니다.
OK
exit=0
NG: 다른 표시가 혼입됨: CANARY-Z9Y8
...
종료 코드(명령어의 성공 여부를 나타내는 숫자)가 0이 아니면 중단하는 방식으로 설정해 두면, CI (Continuous Integration, 지속적 통합)나 모니터링에 그대로 적용할 수 있습니다.
이 예시의 한계점
솔직하게 말씀드립니다.
- 마스킹(伏せ字)은 정규 표현식(Regular Expression, 문자 배열 패턴)이므로, 형태가 다른 기밀 정보는 놓칠 수 있습니다. 완벽한 대책은 아닙니다.
- 표시 검사로는 '표시가 붙은 데이터'가 섞인 경우만 발견할 수 있습니다. 이번 Workers AI 사례처럼, 타인의 표시가 없는 데이터가 섞인 경우에는 감지할 수 없습니다. 그럼에도 불구하고, 자신의 표시가 사라졌거나 응답 내용이 명백히 다른 건이라는 등 이상 징후를 포착하는 단서로는 활용될 수 있습니다.
- 가장 확실한 방법은 기밀을 포함하는 처리를 로컬 LLM (Local LLM, 자신의 머신에서 구동되는 AI 모델)처럼 현지에서 완결시키는 것입니다.
요약
- Workers AI의 GLM-5.3에서 긴 프롬프트에 대한 답변 155건 중 다른 이용자의 데이터가 혼입되었습니다 (2026년 8월 27일~9월 9일, 공식 발표).
- 원인은 설정 불일치와 SGLang의 결함이 겹치면서, 에러가 발생해야 할 부분이 HTTP 200으로 반환된 것입니다. - 이용자 측 대책은 '전송하기 전에 마스킹', '마스킹 위치는 외부와 연결되지 않게', '돌아온 내용을 확인하는 것'입니다. Docker의
--network none
과--read-only
로 최소한의 형태를 만들 수 있습니다.
더 '격리(Isolation)'에 대해 알고 싶은 분께
컨테이너에서 '구동 장소를 분리한다'는 개념은 Zenn의 책 『AI × Docker』에서 순차적으로 작성했습니다. 제1장은 무료로 읽을 수 있습니다.
note에서도 개발 기록을 쓰고 있습니다: https://note.com/fk2000
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기