긴 요청에 다른 고객의 대화가 섞였다: Cloudflare가 놓친 데이터 격리

Cloudflare의 GLM-5.3에서 긴 입력에 다른 고객의 대화가 섞인 응답 155개가 확인됐습니다. 성공 응답만으로 발견하지 못한 메모리 격리 결함과 수정 내용을 살펴봅니다.

내 요청과 응답의 소유권 혼입

긴 문서를 넣고 받은 답변에 다른 고객의 대화가 섞였다. Cloudflare는 10월 26일 공개한 사고 보고서에서 자사 모델 실행 서비스 Workers AI의 GLM-5.3에 고객 간 데이터 격리 결함이 있었다고 밝혔다. 8월 27일부터 9월 9일까지 영향받은 응답은 155개였다. 모두 입력이 681,280토큰보다 긴 요청에서 나왔다. 토큰은 모델이 글을 처리하는 단위로, 글자 수와 일대일로 대응하지 않는다.

Cloudflare가 확인한 범위는 해당 모델이다. 다른 Workers AI 모델은 영향받지 않았고, 회사와 제보자의 시험 요청을 제외한 이용 기록에서는 의도적 악용의 증거를 찾지 못했다고 설명했다. 모델은 9월 9일 수정 후 다시 제공됐으며, 이번 공개는 추론 엔진 SGLang 측과 조율한 결과다.

받겠다고 한 입력보다 메모리가 작았다

GLM-5.3에 설정된 컨텍스트 길이는 1,048,576토큰이었다. 한 번에 다룰 수 있다고 정한 입력·대화 범위다. 그런데 입력을 먼저 계산하는 GPU의 KV 캐시 풀에는 681,280토큰만 들어갔다. KV 캐시는 앞서 읽은 내용을 매번 다시 계산하지 않도록 저장하는 중간 계산값이다. 요청을 받아도 되는 범위와 실제 처리할 공간이 맞지 않았다.

설정된 컨텍스트 1,048,576토큰과 실제 입력 처리 KV 풀 681,280토큰의 차이
출처: Cloudflare 사고 보고서

Cloudflare는 출시 전에 이 차이를 알고 있었다. 공간을 넘는 요청은 엔진이 오류로 돌려줄 것으로 예상했다. 긴 입력 시험에서도 성공 상태 코드가 왔고 스트리밍 응답에 오류 표시가 없어, 내부에서 차이를 처리한다고 판단했다. 시험은 응답 코드와 성능을 확인했지만 답변 내용과 일관성까지 검증하지 않았다.

풀 크기는 컨텍스트 설정값을 그대로 따라가지 않는다. SGLang이 시작할 때 모델 가중치를 올리고 남은 GPU 메모리로 정한다. 모델이나 메모리 사용 방식이 바뀌면 같은 길이를 허용해도 처리 공간은 달라질 수 있다. 시작 로그와 지표에 나온 실제 용량을 대조해야 했던 이유다.

거절된 요청이 왜 답변 생성까지 갔나

Workers AI는 입력을 읽는 단계(prefill)와 답변을 만드는 단계(decode)를 서로 다른 GPU에 나눴다. 앞쪽에서 만든 KV 캐시를 뒤쪽으로 전달하고, 뒤쪽이 그 값을 읽어 답변을 이어 쓴다. GPU 메모리는 고정 크기의 페이지로 나눠 여러 요청이 재사용한다. 매번 내용을 지우기보다는 다음 요청이 덮어쓰는 방식이어서, 어느 요청의 페이지인지와 언제 다시 써도 되는지를 정확히 관리해야 한다.

용량을 넘긴 입력은 내부에서 거절 대상으로 표시됐다. 이때 원래 입력을 한 토큰짜리 임시 입력으로 바꿨는데, 거절 상태가 다음 처리 단계에 제대로 반영되지 않았다. 양쪽의 진입 검사는 요청이 ‘끝났는지’를 봤고, 아직 오류 응답을 보내지 않은 요청을 살아 있는 요청으로 받아들였다. 단일 노드에서는 오류로 끝나던 경로가 분리 구성에서는 계속 진행됐다.

앞쪽 GPU는 임시 입력 한 페이지 분량만 보내고 전송 완료를 알렸다. 뒤쪽 GPU는 원래 긴 입력에 맞춰 공간을 예약한 뒤, 전체가 새 내용으로 채워졌다고 간주했다. Cloudflare의 설명 그림은 이를 여덟 페이지로 예시한다. 첫 페이지만 이번 요청의 값이고 나머지 일곱 페이지에는 이전 고객의 입력과 일부 생성 결과가 남아 있었다. 답변 생성은 여덟 페이지를 모두 읽었다.

원문 기반 한글 번역: 정상 전달과 거절 요청의 잘못된 전달을 8페이지 예시로 비교
원문 기반 한글 번역. 출처: Cloudflare의 정상·거절 요청 비교도

이 응답은 실패 코드가 아닌 HTTP 200으로 돌아왔다. 기록된 입력 토큰 수도 원래 긴 요청과 맞았고, 문장도 그럴듯했다. 긴 입력을 제대로 계산했을 때보다 빨리 도착한 답변에 다른 사람의 내용이 들어간 것이다. 여기서 노출된 것은 모델이 학습하며 외운 문장을 꺼낸 현상이 아니라, 같은 서비스를 조금 먼저 이용한 고객의 요청에서 남은 메모리였다.

용량을 늘리는 것과 실패를 멈추는 것은 별개다

제보는 9월 8일 23시 11분(UTC)에 접수됐다. Cloudflare는 이튿날 검증을 시작했고, 13시 22분 보안 사고를 선언했다. 14시에는 모델을 비공개로 전환해 요청에 HTTP 400을 돌려줬다. 보정한 설정과 엔진 패치를 적용하고 제보자의 재현 스크립트 및 식별 표식을 넣은 시험을 통과한 뒤, 같은 날 19시 40분 서비스를 복구했다.

9월 8일 제보부터 9월 9일 차단·설정 수정·엔진 패치·서비스 복구까지의 UTC 시간선
출처: Cloudflare 대응 시간선

설정을 조정한 뒤 입력 처리 풀은 약 204만 토큰을 담게 됐다. 허용한 컨텍스트 길이보다 커졌으므로 정상 범위의 입력이 해당 거절 경로에 들어가는 일을 막았다. 엔진도 고쳤다. 거절 대기 중인 요청은 메모리 할당이나 캐시 전달에 진입하기 전에 원래 오류와 함께 끝내도록 했다. 용량만 늘리면 향후 설정 변경이나 다른 거절 사유로 같은 경로가 다시 열릴 수 있기 때문이다.

후속 보강에는 전송 도중 취소된 요청의 메모리를 너무 일찍 재사용하지 않는 조치도 포함됐다. 네트워크로 이미 출발한 쓰기는 취소 신호만으로 되돌릴 수 없다. 목적지 페이지를 다른 요청에 넘긴 뒤 늦은 쓰기가 도착하면 그 요청의 캐시를 오염시킨다. 이번 사고의 직접 원인인 거절 처리 결함과는 다른 경로지만, 고객 사이에 메모리가 섞인다는 결과는 같다.

분리형 SGLang을 운영한다면 Cloudflare의 권고는 v0.5.22 이상으로 갱신하고, 설정이 바뀔 때마다 양쪽 노드의 실제 KV 풀과 컨텍스트 길이를 비교하라는 것이다. 캐시 해제 지연과 전송량 제한도 함께 확인해야 한다. 전송 체크섬과 캐시 오염 탐지는 모델별 지원 범위가 달라, 옵션을 켰다는 사실만으로 검증이 작동한다고 볼 수는 없다.

서비스를 도입하는 쪽에서도 ‘학습에 사용하지 않는다’는 정책과 실행 중 고객 간 격리를 구분해서 물을 필요가 있다. 이번 결함은 학습 데이터 정책이 아니라 응답을 만드는 순간의 메모리 소유권에서 생겼다. 공급자가 긴 요청과 실패·취소 경로에서 고객 간 격리를 어떻게 시험하는지, 설정 변경 뒤 어떤 검증을 거치는지가 검토 대상이다.


출처: Cloudflare 사고 보고서, SGLang 보안 업데이트