정상 응답처럼 보였지만 다른 고객의 입력이 들어갔다
AI 모델에 긴 문서를 맡겼는데, 답변 일부가 내가 올린 문서가 아니라 전혀 다른 사람의 대화에서 나온 문장이라면 어떨까. 오류 코드도 없고 답변은 그럴듯하게 이어진다면 사용자는 단순한 모델 환각인지 실제 데이터 노출인지 구분하기 어렵다. Cloudflare가 10월 26일 공개한 GLM-5.3 사고는 이런 상황이 실제 서비스에서 발생했음을 보여 준다.
영향을 받은 것은 Cloudflare Workers AI에서 제공하던 `@cf/zai-org/glm-5.3` 모델이다. Cloudflare 조사에 따르면 8월 27일부터 9월 9일까지 다른 고객의 요청 내용이 섞인 응답 155건이 있었고, 모두 681,280토큰을 넘는 긴 입력에 해당했다. 이는 155명의 고객이 피해를 봤다는 뜻이 아니라 회사가 식별한 응답 건수다.
Cloudflare는 9월 8일 보안 연구자의 신고를 받았고, 다음 날 문제를 재현한 뒤 모델을 일시 중단했다. 설정과 추론 엔진을 고쳐 같은 날 다시 공개했다. 다른 Workers AI 모델에서 동일한 문제가 발생한 증거는 확인되지 않았다고 밝혔다. 따라서 10월 26일은 사고 발생일이 아니라 사후 분석을 공개한 날이다.
핵심은 모델이 원래 다른 사람의 정보를 알고 있어서 답했다는 이야기가 아니다. 서로 다른 고객의 요청을 처리하는 서버가 GPU 메모리를 재사용하는 과정에서, 이전 요청에 속한 계산 결과를 새 요청의 것으로 잘못 읽은 문제다. 실제 노출 범위는 Cloudflare의 조사에 근거하며, 모든 LLM 서비스에 같은 취약점이 있다는 주장으로 확대해서는 안 된다.
KV 캐시는 왜 사용자 요청 사이에서 공유될까
LLM이 문장을 생성할 때는 앞에서 읽은 토큰의 계산 중간값을 계속 참고한다. 이를 KV 캐시라고 부른다. 같은 대화의 앞부분을 매번 처음부터 계산하지 않도록 GPU 메모리에 저장하는 자료라서, 긴 입력이나 동시 접속이 많을수록 캐시를 효율적으로 관리하는 일이 중요하다.
대형 추론 서버는 요청마다 GPU 메모리 전체를 새로 할당하지 않는다. 정해진 크기의 메모리 페이지를 여러 요청에 배분하고, 요청이 끝나면 반납받아 다음 요청에 재사용한다. 성능을 위해 페이지를 매번 0으로 지우는 대신 새 요청의 계산 결과가 기존 내용을 정확히 덮어쓴다는 전제에 의존할 수 있다.
또 하나의 특징은 작업을 나누는 방식이다. 긴 입력을 읽어 KV 캐시를 만드는 단계인 prefill과, 답변을 한 토큰씩 만드는 decode를 서로 다른 GPU 서버에서 처리할 수 있다. SGLang은 이렇게 나뉜 서버 사이에 캐시를 옮겨 높은 처리량을 얻는다. 이번 사고는 바로 이 두 단계가 서로 다른 요청 상태를 믿은 지점에서 생겼다.
이 설계 자체가 곧 보안 결함은 아니다. 정상 경로에서는 새 요청에 할당된 페이지가 모두 새 데이터로 채워지고, 거부된 요청은 답변 생성까지 가지 않는다. 문제는 입력이 메모리 한도를 넘었을 때 그 거부 상태가 다른 서버까지 정확히 전달되지 않았다는 데 있다. 아래 도식은 공식 기술 설명을 토대로 새로 그린 개념도이며 실제 서버 화면은 아니다.
104만 토큰 설정과 68만 토큰 메모리의 틈
Cloudflare가 설정한 GLM-5.3의 최대 문맥 길이는 1,048,576토큰이었다. 하지만 당시 prefill GPU의 KV 캐시 풀이 실제로 수용할 수 있던 양은 681,280토큰뿐이었다. 사용자는 모델의 최대 길이 안에 들어가는 요청을 보냈더라도, 서버 메모리에서는 그 요청 전체를 처리할 공간이 없을 수 있었다.
서버가 이때 정상적으로 입력을 거절했다면 개인정보 노출로 이어지지 않았을 것이다. SGLang 스케줄러는 긴 요청을 거부 대상으로 표시하고 내부 처리를 마무리하기 위해 한 토큰짜리 임시 입력으로 바꿨다. 단일 서버 경로에서는 최종적으로 오류가 반환됐지만, prefill과 decode가 분리된 경로에서는 거부 상태를 확인하는 방식이 달랐다.
decode 서버는 원래 요청 길이에 맞춰 캐시 페이지 여러 개를 확보한 상태였다. prefill에서는 임시 입력 한 토큰의 캐시만 만들어 전송했는데도, decode는 전송 완료 신호를 받고 나머지 페이지까지 이번 요청의 데이터가 채워졌다고 믿었다. 실제로 덮어쓰지 않은 페이지에는 직전 요청들의 캐시 내용이 남아 있었다.
그 결과 새 요청의 응답을 생성하면서 과거 요청의 페이지까지 참조했다. HTTP 상태는 오류가 아닌 200으로 돌아왔고, 사용량에 표시된 입력 토큰 수도 긴 요청과 일치할 수 있었다. 단순히 HTTP 성공률과 응답 시간만 보는 테스트로는 발견하기 어려웠던 이유다. Cloudflare는 이후 실제 실행 중인 엔진의 캐시 수용량과 설정 문맥 길이를 직접 대조하는 검사를 추가했다고 설명한다.
| 항목 | 사고 당시 값 | 의미 |
|---|---|---|
| 설정된 문맥 길이 | 1,048,576토큰 | 서비스가 받도록 설정한 최대 입력 |
| prefill KV 캐시 풀 | 681,280토큰 | GPU에서 실제 수용 가능한 양 |
| 영향이 확인된 응답 | 155건 | Cloudflare가 식별한 응답 수 |
| 수정 후 prefill 풀 | 약 204만 토큰 | 설정 문맥 길이를 충분히 수용 |
Cloudflare가 2026년 10월 26일 공개한 GLM-5.3 배포 설정과 사고 조사 결과. 다른 서버나 모델의 일반적인 수치는 아니다.
한 번의 오류가 어떻게 사용자 경계를 넘었나
첫 단계에서 라우터는 긴 요청을 prefill과 decode 양쪽에 보낸다. decode는 전달받을 캐시를 저장할 페이지를 미리 예약한다. prefill은 메모리 부족으로 입력을 거부 대상으로 바꾸지만, 이전 SGLang 구현의 입장 대기열은 그 요청이 완료됐는지만 확인하고 거부 예정인지는 제대로 막지 못했다.
두 번째 단계에서 prefill은 원래 입력 대신 한 토큰짜리 임시 입력을 처리한다. 그리고 작은 캐시 일부만 decode로 옮긴 뒤 전송이 끝났다는 신호를 보낸다. decode는 자신이 예약한 페이지 전체가 이번 요청에 해당한다고 간주해 답변 생성을 시작한다. 캐시 페이지의 소유권과 실제로 기록된 범위가 어긋난 것이다.
마지막 단계에서 decode의 attention 계산은 이번 요청이 기록하지 않은 페이지도 읽는다. 그 페이지들은 이전 요청에서 사용한 내용이 완전히 덮어써지지 않은 상태다. 따라서 답변에 다른 고객의 문맥이 섞일 수 있다. 공격자가 다른 고객의 특정 요청을 선택해 읽었다는 뜻은 아니며, Cloudflare는 조사에서 의도적인 악용의 증거를 찾지 못했다고 밝혔다.
이 사고와 별개로 SGLang 팀은 요청 중단 직후에도 이미 시작된 RDMA 전송이 계속될 수 있는 문제를 함께 설명했다. 메모리 페이지를 너무 빨리 반납하면 늦게 도착한 데이터가 다음 요청의 캐시를 덮어쓸 수 있다. 앞의 문제는 거부된 요청이 처리 경로를 통과한 오류이고, 뒤의 문제는 비동기 전송 중 페이지 수명 관리 오류다. 원인은 구분해야 하지만 두 경우 모두 사용자별 메모리 경계를 지키는 것이 핵심이다.
Cloudflare 복구와 SGLang 0.5.22에서 달라진 처리
Cloudflare는 9월 9일 GLM-5.3을 잠시 비공개로 돌리고 KV 풀 설정을 수정했다. 수정 후 prefill 풀이 약 204만 토큰을 담을 수 있어, 설정된 약 104만 토큰의 문맥 길이보다 넉넉해졌다. 정상 범위의 입력이 캐시 부족으로 거부되는 상황 자체를 없앤 조치다. 하지만 서버 설정은 모델 교체나 양자화 방식 변경에 따라 달라질 수 있으므로 이 방법만으로는 충분하지 않다.
엔진 자체에도 안전장치를 넣었다. SGLang의 수정은 거부된 요청이 prefill과 decode 사이의 연결 절차로 넘어가기 전에 종료되도록 한다. #36988은 거부 요청을 처리 결과 단계에서 정리하고, #38935는 더 앞선 입장 단계에서 막는다. 한 토큰 임시 입력을 전달하고 decode가 잘못된 페이지를 읽는 경로를 끊는 것이다.
Cloudflare는 운영자에게 SGLang v0.5.22 이상을 권장한다. 공식 릴리스 기록상 v0.5.22는 10월 19일 배포됐으며 Python 3.11 이상을 요구한다. 일부 안전장치는 v0.5.20과 v0.5.21부터 들어갔지만, 전송 백엔드와 실패 유형에 따라 필요한 변경이 달라 일괄적으로 예전 버전이 안전하다고 단정할 수 없다.
SGLang 팀은 중단된 요청의 페이지를 즉시 반납하지 않고, 전송이 끝났다는 확인 또는 제한된 대기 시간까지 보유하는 deferred release도 설명했다. 비동기 전송이 끝나기 전에 페이지를 다른 요청에 내주는 일을 막기 위한 조치다. 추가 검증 기능인 `--disaggregation-enable-kv-checksum`은 지원 모델과 배포 조건을 확인해 선택적으로 켜야 하며, 모든 아키텍처에서 자동으로 동작하는 기능은 아니다.
SGLang 추론 서버를 운영한다면 확인할 다섯 가지
첫째, 지금 배포한 SGLang의 정확한 버전과 전송 구성을 기록한다. prefill과 decode를 분리해 운영하는지, Mooncake·NIXL·MORI 중 무엇을 쓰는지에 따라 영향을 받는 경로가 다르다. 단일 서버의 오류 처리와 분산 구성의 오류 처리를 같은 것으로 가정하지 않는다. 특히 오래된 이미지 태그를 고정해 둔 환경이라면 실행 중인 버전부터 확인해야 한다.
둘째, 각 prefill·decode 노드에서 실제 KV 캐시 풀 크기를 확인한다. 설정 파일에 적힌 최대 문맥 길이만 보는 것이 아니라 엔진 시작 로그와 메트릭에 나온 실제 수용량을 대조해야 한다. 모델 가중치, 양자화, GPU 병렬화, CUDA 그래프 설정을 바꾼 뒤에도 다시 확인한다. 풀이 문맥 길이보다 작다면 우선 서비스 문맥 길이를 안전하게 낮추거나 메모리 배치를 조정해야 한다.
셋째, SGLang v0.5.22 이상으로 올리는 경로를 검토한다. 공식 릴리스는 Python 3.11 이상을 요구하며, NVIDIA CUDA 13 환경의 공식 컨테이너 예시는 `lmsysorg/sglang:v0.5.22`다. 운영 중인 모델과 GPU 백엔드의 호환성 검증 없이 컨테이너만 바꾸는 것은 위험하다. 이 글은 실제 업그레이드를 수행한 결과가 아니라 공식 릴리스에 근거한 점검 순서다.
넷째, 거부와 중단 상황을 테스트한다. 정상 길이의 요청만 보내서 HTTP 200을 확인하는 것으로 끝내지 말고, 메모리 한도 근처의 요청과 중단된 전송이 어떻게 처리되는지 별도 격리 환경에서 살펴야 한다. 운영 중인 다중 고객 서버에서 데이터 노출을 재현하려고 무리하게 초장문 요청을 보내는 방법은 권장하지 않는다. 오류 코드뿐 아니라 답변의 일관성과 캐시 소유권 검증도 중요하다.
다섯째, 업그레이드 후에도 전송 완료와 페이지 반납 관련 메트릭을 본다. SGLang 팀은 v0.5.22의 `sglang:decode_deferred_kv_release_total{outcome}`를 확인하라고 안내한다. 정상 환경이라면 대다수 보류 요청이 전송 완료 신호로 해제돼야 하며, 제한 시간 초과가 많다면 양쪽 서버의 확인 신호 경로를 점검할 필요가 있다. 이 지표 하나로 과거 노출이 없었다고 증명할 수는 없다.
# SGLang v0.5.22 공식 릴리스의 설치 예시 (실행하지 않은 문서 기반 예시)
uv pip install --prerelease=allow sglang==0.5.22
# NVIDIA CUDA 13 공식 이미지 예시
docker pull lmsysorg/sglang:v0.5.22Cloudflare 사용자와 자체 호스팅 운영자가 해야 할 일은 다르다
Cloudflare Workers AI에서 해당 모델을 호출한 사용자라면 이번 사고가 이미 9월 9일에 복구됐다는 점부터 구분해야 한다. Cloudflare는 다른 Workers AI 모델에서는 동일한 문제가 확인되지 않았고 의도적인 악용 증거도 없다고 밝혔다. 다만 사고 기간에 GLM-5.3으로 681,280토큰을 넘는 입력을 처리했다면 공식 보고서의 영향 범위를 확인하고, 민감한 자료를 보냈는지 내부 기록을 점검할 이유가 있다.
반대로 SGLang을 직접 운영한다면 Cloudflare의 설정 수정만 보고 안심할 수 없다. 같은 엔진이라도 GPU 구성, 메모리 할당, 요청 중단 경로, 라우터와 전송 백엔드가 다르다. Cloudflare가 자기 서비스에서 발견한 155건이라는 숫자는 다른 업체의 위험도를 계산하는 기준이 아니다. 자체 환경의 버전과 캐시 페이지 수명 관리가 별도의 점검 대상이다.
AI 앱 개발자 입장에서는 제공업체가 '최대 100만 토큰 지원'이라고 표기했을 때 그것이 실제 모든 요청에서 안전하게 처리된다는 뜻인지 구분할 필요가 있다. 문맥 길이는 모델 설정이고 KV 캐시 풀은 실제 GPU 메모리의 자원이다. 두 값이 다를 때 오류를 어떻게 돌려주는지, 고객별 요청을 어떻게 격리하는지는 서비스 운영의 문제다.
이번 공개는 사고를 과장하기보다 어떤 검사가 빠졌는지를 보는 데 가치가 있다. 성공 응답과 빠른 처리량을 확인한 테스트가 메모리 경계까지 검증해 주지는 않는다. LLM 추론 서버를 운영한다면 정상 요청뿐 아니라 거부·중단·재시도 상황에서 이전 요청의 상태가 새 요청으로 넘어가지 않는지 확인해야 한다.
자주 묻는 질문: 영향 범위와 업그레이드 판단
사고 보고서의 숫자는 특정 Cloudflare 배포와 기간에 한정된다. 모델을 사용했다는 사실만으로 모든 요청이 노출된 것은 아니고, SGLang을 설치했다는 이유만으로 동일한 결함이 항상 재현되는 것도 아니다. 어떤 요청 경로가 영향을 받았는지와 현재 적용된 수정 사항을 분리해 읽어야 한다.
Cloudflare가 공개한 것은 자사 사고 조사와 SGLang 팀의 공동 수정 내용이다. 본문에서 소개한 도식과 운영 점검 순서는 이 공식 자료를 바탕으로 재구성했으며, BlueBWorks가 실제 피해 데이터를 확인하거나 별도 침투 테스트를 수행한 결과는 아니다.
GLM-5.3 모델은 지금도 위험한가?
Cloudflare는 2026년 9월 9일 설정과 엔진을 수정해 모델을 다시 공개했다고 밝혔다. 10월 26일 문서는 과거 사고의 공개 보고서이며, 현재 동일 문제가 계속 발생한다는 근거는 없다.
SGLang은 어떤 버전으로 올려야 하나?
Cloudflare는 v0.5.22 이상을 권장한다. 해당 릴리스는 2026년 10월 19일 공개됐고 Python 3.11 이상을 요구한다. 실제 적용 전에는 사용하는 모델과 전송 백엔드의 호환성을 확인해야 한다.
다른 Workers AI 모델도 고객 데이터가 섞였나?
Cloudflare 조사에서는 GLM-5.3 외 다른 Workers AI 모델이 같은 사고의 영향을 받은 것으로 확인되지 않았다. 이 결과를 다른 클라우드 사업자나 자체 운영 서버에 일반화할 수는 없다.
HTTP 200이면 KV 캐시 문제가 없다고 봐도 되나?
아니다. 이번 사고에서는 잘못된 요청이 HTTP 200으로 처리되면서 다른 고객의 캐시가 응답에 섞일 수 있었다. 정상 상태 코드뿐 아니라 입력 거부와 캐시 소유권, 출력 일관성을 확인해야 한다.