본문으로 바로가기
Trend

4 KiB 썼는데 60 KiB가 딸려 왔다 — Cloudflare 컨테이너 잔여 데이터 노출의 전말

Cloudflare가 Containers·Sandboxes의 잔여 블록 노출 취약점을 수정했다. 64 KiB 블록과 skip_block_zeroing이 만든 구멍, 두 단계 완화, 그리고 에이전트 샌드박스를 쓰는 1인 개발자가 점검할 항목을 짚는다.

공유 저장 풀에서 반환된 블록이 새 컨테이너로 넘어가며 앞부분만 새 데이터로 채워진 모습을 그린 개념 그림
4 KiB 썼는데 60 KiB가 딸려 왔다 — Cloudflare 컨테이너 잔여 데이터 노출의 전말 표지 삽화
AI 생성출처: AI 생성 삽화(Codex 이미지 생성)

Cloudflare가 Cloudflare Containers와 그 위에 올라간 Cloudflare Sandboxes에서 테넌트 경계를 넘는 잔여 데이터 노출 취약점을 수정했다. 방아쇠는 씬 프로비저닝 풀에 걸려 있던 skip_block_zeroing 설정이었고, 수정은 Containers 전체 서버에 적용됐다. 고객이 설정을 바꾸거나 배포를 다시 할 일은 없고, 악의적 악용 증거도 확인되지 않았다. 다만 에이전트가 만든 파일을 컨테이너 로컬 디스크에 그대로 두고 있다면 이번 공지는 한 번 읽어 둘 값이 있다. 경과와 수치는 Cloudflare의 공지 글에 공개됐다.

삭제된 컨테이너의 물리 블록이 여러 계정을 함께 담당하는 풀로 돌아갔다

Containers는 Linux device mapper 씬 프로비저닝(dm-thin)으로 컨테이너마다 쓰기 가능한 루트 디스크를 준다. 각 컨테이너는 Firecracker 가상 머신 모니터가 띄운 전용 가상 머신 안에서 돌고, 이 디스크는 게스트에 /dev/vdc로 노출된다. 씬 프로비저닝은 가상 디스크가 아직 매핑되지 않은 영역에 쓸 때 비로소 물리 저장 공간을 할당한다. 문제가 된 풀의 씬 블록 크기는 64 KiB였고, 컨테이너 루트 디스크를 받치던 씬 볼륨이 삭제되면 그 물리 블록은 여러 고객 계정의 워크로드를 함께 담당하는 풀로 반환됐다.

해당 풀에는 다음 옵션이 들어 있었다.

skip_block_zeroing

이 옵션이 걸려 있으면 dm-thin은 새로 할당한 블록을 컨테이너에 노출하기 전에 0으로 채우는 단계를 건너뛴다.

가운데 공유 블록 풀을 두고 왼쪽에서 삭제된 컨테이너가 블록을 반환하고 오른쪽 새 가상 머신이 그 블록을 받는 개념 그림
삭제된 씬 볼륨의 물리 블록이 여러 계정의 워크로드를 함께 담당하는 풀로 돌아가 재할당되는 관계를 그린 개념 그림이다. 실제 화면이 아니다.
AI 생성출처: AI 생성 삽화(Codex 이미지 생성)

재사용 블록에 4 KiB를 쓰면 나머지 60 KiB에 이전 데이터가 남을 수 있었다

조건이 겹쳐야 했다. 쓰기가 아직 매핑되지 않은 씬 블록에 닿아 dm-thin이 공유 풀에서 물리 64 KiB 블록을 할당해야 했고, 그 정렬 영역에 4 KiB만 쓰여 나머지가 덮이지 않아야 했고, 풀에 블록 초기화가 꺼져 있어야 했다. 잔여 데이터가 항상 존재한 것도 아니었다.

새 씬 디스크의 매핑되지 않은 영역을 그냥 읽어도 잔여 데이터는 나오지 않았다. 이 경우 dm-thin은 물리 블록을 할당하지 않고 0을 돌려준다. 증명 코드는 게스트 ext4 파일시스템의 빈 공간에 해당하는 64 KiB 정렬 영역을 찾아, 각 영역에 정렬된 4 KiB 블록 하나씩을 써 넣었다. 그 쓰기가 매핑되지 않은 씬 블록에 닿으면 dm-thin이 공유 풀에서 물리 64 KiB 블록을 할당했고, 4 KiB만 새 내용으로 덮이면서 블록 초기화가 꺼져 있던 탓에 남은 60 KiB에는 이전 컨테이너의 데이터가 남을 수 있었다.

증명 코드가 밟은 단계는 이렇다.

  • Workers Paid 계정으로 컨테이너를 만든다.
  • 쓰기 가능한 루트 디스크 /dev/vdc를 연다.
  • 디스크를 읽어 기준값을 기록한다.
  • ext4 빈 공간에 대응하는 64 KiB 영역마다 4 KiB 블록 하나를 쓴다.
  • 결과 블록을 다시 읽는다.
  • 새 컨테이너가 덮어쓰지 않은 부분만 살펴본다.
새로 쓴 4 KiB, 이전 데이터가 남을 수 있는 60 KiB, 블록 전체 64 KiB를 비교한 막대 그래프
정렬된 4 KiB 쓰기 하나가 64 KiB 물리 블록 할당을 유발하면, 덮이지 않은 나머지 60 KiB에 이전 컨테이너의 데이터가 남을 수 있었다.
도식출처: 근거를 바탕으로 코드가 그린 도식

테스트 가능한 디렉터리 블록 5,614개 중 연구진 것은 0개였다

연구진은 ext4의 metadata_csum 기능이 디렉터리 블록 체크섬에 파일시스템·아이노드 관련 값을 넣는 성질을 이용해, 증명용으로 직접 만든 파일시스템의 블록과 다른 파일시스템에서 온 블록을 구분했다. 프로덕션 배치 6곳에서 보고된 수치는 다음과 같다.

항목값
테스트 가능한 디렉터리 블록5,614개 전부
연구진 파일시스템으로 귀속된 블록0개
체크섬 분석으로 식별된 외부 디렉터리 아이노드2,700개
통제 테스트에서 올바르게 귀속된 블록162개

최종적으로는 배치 24곳 중 18곳, 기반 노드 22개 중 20개에서 잔여 자료가 관측됐고 그 범위는 4개 대륙에 걸쳐 있었다. 복구된 블록 유형에는 디렉터리 구조, 데이터베이스 페이지, 구조적으로 완전한 SQLite 데이터베이스가 있었다. 연구진은 집계 수치와 형식 검사만 출력하는 스크립트를 썼다고 보고했고, 제출물에 제3자 식별자나 복구된 내용 값은 담기지 않았다. 손에 남은 데이터는 HackerOne 공개 정책에 맞춰 안전하게 삭제했다고 확인했다.

초기화를 되살린 뒤에도 이미 매핑된 디스크와 이미지 캐시가 남았다

첫 완화는 전체 서버의 dm-thin 풀 설정에서 skip_block_zeroing을 제거해, 새로 할당한 블록을 컨테이너에 넘기기 전에 지우는 기본 동작을 되살린 것이다. 연구진은 이 변경 이후 자신들의 증명 코드가 더 이상 동작하지 않는다고 따로 확인해 줬다. 하지만 새 할당을 초기화하는 조치는 이미 씬 장치에 매핑된 블록까지 청소하지는 못했다. 그런 매핑은 실행 중인 컨테이너 디스크와, 호스트마다 OCI 이미지 레이어용으로 준비해 둔 dm-thin 스냅숏 캐시에 남아 있었다.

그래서 Cloudflare는 실행 중인 컨테이너 디스크를 모두 폐기하고 완화 전에 만들어진 캐시 스냅숏을 지웠다. 비혼잡 시간대에 호스트를 비우고 각 호스트의 가상 머신을 재시작하며 이미지 캐시를 정리해, 디스크와 캐시된 레이어가 초기화된 할당으로 다시 만들어지게 했다. 이 정리는 Containers 전체 서버에서 끝났다.

skip_block_zeroing 제거에서 이미지 캐시 정리까지 다섯 단계를 이은 순서 도식
새 할당 초기화를 되살린 뒤에도 이미 매핑된 디스크와 캐시된 이미지 레이어가 남아, 디스크 폐기와 캐시 정리까지 이어졌다.
도식출처: 근거를 바탕으로 코드가 그린 도식

보고부터 캐시 정리 완료까지의 경과는 아래와 같다. 런타임 수정은 보고가 들어온 9월 4일에 병합됐고, 완화 전 캐시 스냅숏 정리는 9월 19일 15:03 UTC에 끝났다.

9월 4일 제보부터 9월 19일 캐시 스냅숏 정리 완료까지 시점과 사건을 나열한 시간축
보고 당일 런타임 수정이 병합됐고, 롤아웃이 끝난 뒤 캐시 스냅숏 정리가 마지막에 완료됐다.
도식출처: 근거를 바탕으로 코드가 그린 도식

디스크 I/O 기록에 탐지 시그니처를 걸었지만 다른 활동은 잡히지 않았다

증명 코드는 쓰기와 읽기 사이에 특징적인 관계를 남긴다. 4 KiB 쓰기가 매핑되지 않은 영역에 닿아 재사용된 64 KiB 블록의 할당을 유발하면, 이어지는 읽기가 새 컨테이너가 덮어쓴 양보다 훨씬 많은 데이터를 가져올 수 있다. Cloudflare는 이 특징으로 탐지 시그니처를 만들어 보관 중인 과거 디스크 I/O 텔레메트리에 적용했다. 잡힌 활동은 연구진과 인가된 검증을 하던 Cloudflare 엔지니어의 것이었고, 그 밖에 이 기법과 일치하는 활동은 확인되지 않았다.

영향 범위도 함께 짚어 둘 만하다. 공격자는 특정 피해자를 고를 수 없었고 활성 상태로 붙어 있는 디스크에는 접근하지 못했으며, 노출 여부는 Cloudflare의 워크로드 배치와 dm-thin이 어떤 해제 블록을 재할당하는지에 달려 있었다. 다른 고객의 활성 데이터를 수정하거나 워크로드 가용성에 영향을 주는 시연은 없었다.

에이전트에게 코드를 실행시키는 샌드박스는 남이 쓰던 블록 위에 앉는다

Cloudflare Sandboxes는 Containers 위에 만들어졌다. AI 에이전트가 생성한 코드를 격리해 돌리려고 이 조합을 골랐다면, 이번 수정 범위 안에 들어 있고 별도로 할 일은 없다. 의미가 있는 쪽은 다른 데 있다. 컨테이너는 전용 Firecracker 가상 머신 안에서 돌지만, 루트 디스크를 받치던 씬 볼륨이 삭제되면 그 물리 블록은 여러 계정의 워크로드를 함께 담당하는 풀로 돌아가 재할당됐다. 전용 가상 머신을 써도, 재할당되는 물리 블록의 잔여 데이터를 제거하는 일은 블록 초기화에 달려 있었다.

혼자 제품을 운영하는 입장에서 꺼내 볼 항목을 정리하면 다음과 같다.

  • 에이전트 작업 디렉터리를 "지우면 사라진다"로 가정하지 않는다. 이번에 복구된 블록에는 구조적으로 완전한 SQLite 데이터베이스가 있었다. 에이전트가 만든 로컬 SQLite 파일, 클론한 저장소, 설정 파일 사본을 컨테이너 안에 평문으로 남기는 흐름이라면 그 수명을 다시 확인할 대상이다.
  • "초기화를 건너뛰는" 최적화 목록을 직접 만들어 본다. 이번 사고의 원인은 dm-thin의 기본 초기화를 끄는 옵션이었다. 자기 파이프라인에서 재사용하는 볼륨, 캐시된 베이스 이미지, 요청 사이에 살아남는 워커 프로세스가 사용자 경계를 넘어 재사용되는지 점검하면 같은 종류의 구멍을 찾을 수 있다.
  • 수정이 캐시까지 닿는지 묻는다. Cloudflare도 설정을 되돌린 것으로 끝내지 못하고 실행 중 디스크 폐기와 이미지 스냅숏 캐시 삭제까지 해야 했다. 비밀 값을 바꾸거나 데이터를 지울 때 빌드 캐시, 컨테이너 이미지 레이어, 프리뷰 배포에 이전 값이 남는지 같은 질문을 던져 보자.
  • 여러 사용자의 작업을 한 컨테이너에서 돌려 쓰고 있는지 본다. 호스트 공유와 블록 재사용이라는 패턴은 규모가 작아도 재현된다. 컨테이너를 새로 만드는 것만으로 잔여 데이터가 사라진다고 가정하지 말고, 재사용되는 블록의 초기화와 기존 이미지 스냅숏 정리 여부를 함께 확인하는 편이 낫다. 이번 사건에서도 새 컨테이너가 캐시된 레이어의 매핑을 물려받으면 쓰이지 않는 영역의 잔여 바이트가 그대로 읽힐 수 있었다.
  • 공지에 적힌 사실만 판단 자료로 쓴다. 기법상 피해자·워크로드·호스트·데이터를 겨냥할 수 없었고 활성 상태로 붙어 있는 디스크에는 접근하지 못했다. 공지에서 확인할 수 있는 판단 자료는 수정 완료, 고객 추가 조치 불필요, 표적 선택 불가능, 보관된 텔레메트리에서 이 기법과 일치하는 추가 활동 미확인이다.

버그 바운티로 들어온 보고 하나가 당일 런타임 수정으로 이어지고, 15일 뒤 캐시 정리까지 끝난 뒤 공개됐다. 플랫폼을 빌려 쓰는 쪽에서 할 수 있는 일은, 이런 공지가 나왔을 때 "우리 쪽에 남는 데이터는 무엇인가"를 확인하는 습관을 만드는 것이다.

이 글의 근거

  1. E1

    공식 자료

    Cloudflare 블로그 공식 원문(How Cloudflare addressed a cross-tenant data exposure vulnerability in Containers)

    공식 자료 수집 · blog.cloudflare.com · · SHA-256 08a2e1c02d7f

이 근거로 확인한 주장

  1. 2026년 9월 4일 Accomplish의 보안 연구자 Oren Yomtov가 Cloudflare 버그 바운티로 Cloudflare Containers와 그 위에 구축된 Cloudflare Sandboxes에 영향을 주는 취약점을 제보했고, Cloudflare는 이를 완전히 수정했으며 고객 데이터가 침해된 증거는 없다. E1
  2. Workers Paid 계정을 가진 고객이 같은 호스트에서 다른 Containers가 쓰던 잔여 디스크 블록을 복구할 수 있었으나, 특정 고객·워크로드·호스트·데이터를 겨냥할 수는 없었고 잔여 데이터가 항상 존재한 것도 아니었다. E1
  3. 문제가 된 저장 풀의 씬 블록 크기는 64 KiB였고 skip_block_zeroing 옵션으로 새 블록 초기화가 꺼져 있어, 4 KiB 쓰기는 블록의 그 부분만 덮고 남은 60 KiB에는 이전 컨테이너의 데이터가 남을 수 있었다. E1
  4. 프로덕션 배치 6곳에서 테스트 가능한 디렉터리 블록 5,614개 전부가 대상이었고 그중 연구진 파일시스템으로 귀속된 것은 0개, 체크섬 분석으로 식별된 외부 디렉터리 아이노드는 2,700개였으며, 최종적으로 배치 24곳 중 18곳과 노드 22개 중 20개에서 4개 대륙에 걸쳐 잔여 자료가 관측됐다. E1
  5. 복구된 블록 유형에는 디렉터리 구조, 데이터베이스 페이지, 구조적으로 완전한 SQLite 데이터베이스가 포함됐고, 통제된 테스트 파일시스템에서 만들었다 삭제한 블록 162개는 모두 올바르게 귀속됐다. E1
  6. 첫 완화는 전체 서버의 dm-thin 풀 설정에서 skip_block_zeroing을 제거한 것이며, 이어서 실행 중인 컨테이너 디스크를 폐기하고 완화 전 캐시 스냅숏을 지우면서 호스트를 비운 뒤 가상 머신을 재시작하고 이미지 캐시를 정리했다. E1
  7. Cloudflare는 과거 디스크 I/O 텔레메트리에 탐지 시그니처를 적용해 이 기법과 일치하는 다른 활동을 찾지 못했고, 수정에 따른 추가 고객 조치는 필요하지 않다고 밝혔다. E1
  8. 9월 4일 21:27 UTC에 런타임 수정과 재사용 테스트가 병합됐고, 9월 19일 15:03 UTC에 영향 범위 전체 서버에서 완화 전 캐시 스냅숏 정리가 완료됐다. E1
  9. 컨테이너는 Firecracker 가상 머신 안에서 실행되며 dm-thin 씬 프로비저닝으로 만든 쓰기 가능한 루트 디스크가 게스트에 /dev/vdc로 노출된다. E1