200개 이상의 셀프 호스팅 AI 도구를 소스 리뷰한 결과, 78개에서 테넌트 간 격리 결함 발견
요약
200개 이상의 셀프 호스팅 AI 및 SaaS 제품을 소스 리뷰한 결과, 78개 제품에서 테넌트 간 데이터 격리 결함이 발견되었습니다. 주로 읽기 엔드포인트에서 권한 부여 확인이 누락되어 타인의 데이터를 노출하는 'read sibling' 패턴이 확인되었습니다.
핵심 포인트
- 200개 이상의 AI/SaaS 제품 중 78개에서 테넌트 간 데이터 노출 결함 발견
- 주요 원인은 쓰기 경로와 달리 읽기 경로에서 권한 확인을 생략하는 패턴
- 발견된 84건의 사례 중 31건은 GitHub Security Advisories에 등록됨
- Sectum AI 도구를 활용하여 합성 데이터를 통한 격리 검증 수행
200개 이상의 멀티 테넌트 (multi-tenant) AI 및 SaaS 제품 중 78개에서 동일한 격리 결함, 즉 수정되지 않은 'read sibling' 결함이 발견되었습니다. 이 패턴과 배포된 수정 사항, 그리고 자신의 제품을 확인하는 방법을 설명합니다.
LLM (Large Language Models)을 기반으로 구축하는 모든 팀은 결국 동일한 기능인 워크스페이스 (workspaces)를 출시하게 됩니다. 테넌트 (Tenants), 팀, 프로젝트, 조직 등 무엇이라 부르든 약속은 동일합니다. 귀하의 지식 베이스, 채팅 기록, 문서가 동일한 인스턴스 내의 다른 모든 사람으로부터 격리되어 보호된다는 것입니다.
그 벽은 멀티 테넌트 제품이 절대로 틀려서는 안 되는 단 한 가지 요소입니다. 그래서 지난 몇 달 동안 저는 그 벽이 어디에서 균열이 생기는지 체계적으로 찾아 나섰습니다.
저는 특정 유형의 버그를 찾기 위해 200개 이상의 멀티 테넌트 AI 및 SaaS 제품을 소스 리뷰 (source-reviewed) 했으며, 그중 78개에서 테넌트 간 데이터 노출 (cross-tenant data exposure)을 확인했습니다. 즉, 한 테넌트가 다른 테넌트의 데이터를 읽을 수 있는(경우에 따라 수정하거나 삭제할 수도 있는) 상황입니다. 이는 78개 제품에서 총 84개의 발견 사항으로 이어졌으며, 그중 31개는 현재 GitHub Security Advisories로 등록되었습니다. 거의 모든 사례가 동일한 실수였으며, 이는 읽기 엔드포인트 (read endpoints)에서 발생합니다.
이 포스트는 해당 실수가 무엇인지 설명하고, 이미 수정된 제품들을 명시하며, 나머지 제품들의 실시간 목록을 안내합니다. 이름이 밝혀지지 않은 모든 제품은 수정 사항이 배포될 때까지 비공개로 유지됩니다. 사용자들이 보호받기 전에 공격자에게 실시간 타겟을 넘겨줄 수는 없기 때문입니다.
테스트 방법
200개 이상의 심층 실험 (deep labs)은 혼자 할 수 없지만, 200개 이상의 소스 리뷰 (source reviews)는 가능하기에 두 단계로 나누어 진행했습니다.
전수 조사 (200개 이상의 제품). 각 후보군에 대해 저는 한 가지 패턴의 코드를 읽었습니다. 바로 쓰기 경로 (write path)에서는 강제되지만, 인접한 읽기 경로에서는 생략되는 권한 부여 확인 (authorization check) 패턴입니다. 대부분의 제품은 이 버그가 없거나, 오픈 소스 버전이 실제로 멀티 테넌트가 아니거나, 이미 모든 형제(sibling) 관계에 대해 수정을 완료한 상태였습니다. 이들이 대다수를 차지하며, 이 수치가 78이라는 결과가 갖는 의미를 뒷받침하는 분모가 됩니다.
확인 (발견 사항). 소스 리뷰에서 실제 격차(gap)가 발견되면, 이를 실제로 확인했습니다:
- 깨끗한 셀프 호스팅 인스턴스를 구축했습니다 (Docker를 사용하여 본인의 하드웨어에서 실행).
- 합성 계정(synthetic accounts)과 합성 카나리 데이터(synthetic canary data)를 사용하여 두 개의 테넌트 A와 B를 생성했습니다. 실제 사용자나 운영 시스템은 전혀 사용하지 않았습니다.
- 오픈 소스 멀티 테넌트 격리 검증 도구인 Sectum AI를 사용하여 테넌트 간 읽기, 수정 또는 삭제를 재현했으며, 이를 데이터베이스의 실제 값(ground truth)과 대조하여 확인했습니다.
- 서명된 (RFC-3161 타임스탬프가 찍힌) 증거 팩을 캡처하여, 조정된 공개(coordinated disclosure, 90일 유예 기간) 원칙에 따라 유지 관리자에게 비공개로 보고했으며, 대개 수정 사항을 첨부했습니다.
마지막 단계 때문에 아래의 점수판은 대부분 비어 있습니다. 발견된 84건 중 대부분은 아직 조정된 공개 절차를 밟고 있습니다. 유지 관리자들에게 통보되었고 수정 사항이 배포되고 있으므로, 저는 이미 공개된 항목들만 이름을 명시하고 나머지는 합산하여 표시했습니다. 전체 현재 목록과 각 권고 사항(advisory)은 공개되는 대로 sectum.ai/research에서 확인할 수 있습니다.
패턴: 쓰기는 보호되지만, 읽기는 보호되지 않음
이번 전수 조사가 거의 기계적일 정도로 수월했던 이유는 동일한 형태가 계속 반복되었기 때문입니다. 쓰기 경로(write path)는 테넌트 범위로 올바르게 제한되어 있었지만, 바로 옆에 있는 읽기 경로(read path)는 그렇지 않았습니다.
개발자가 "이 항목을 삭제"하는 기능을 추가하면서 소유권 확인을 잊지 않았다고 가정해 봅시다: '이 항목이 호출자의 테넌트에 속하는가?' 좋습니다. 그다음 누군가가 "이 항목 보기" 또는 "이 항목의 임베딩(embeddings) 목록 가져오기" 기능을 추가할 때, 해당 확인 절차가 복사되지 않습니다. 목록/가져오기/검색 엔드포인트(endpoint)는 테넌트 필터 없이 원시 ID(raw ID)로 데이터를 가져옵니다. ID는 종종 순차적인 정수이거나 추측 가능한 형태이므로, "5번 항목 보기"를 요청하면 다른 테넌트의 5번 항목을 아무런 문제 없이 반환합니다.
저는 이러한 소급 적용되지 않은 읽기 형제들(un-retrofitted read siblings)이라고 부르기 시작했습니다. 하나를 발견하고 나면 나머지는 grep으로 찾아낼 수 있습니다. 소유권을 확인하는 엔드포인트를 찾은 다음, 확인 절차가 없는 인접 엔드포인트들을 살펴보는 식입니다. 78개의 제품에 걸쳐 거의 매번 동일한 형태가 나타났습니다. 이것은 84개의 서로 다른 버그가 아닙니다. 하나의 버그가 84번 반복된 것입니다.
두 번째로 더 고약한 변종이 있습니다. 검사는 실행되지만, 엉뚱한 대상을 대상으로 실행되는 경우입니다. 핸들러(handler)가 요청한 객체(object)가 아니라 사용자가 제공한 경로(path)를 승인해 버립니다. 사용자가 접근 권한이 있는 자신의 워크스페이스(workspace) ID와 피해자의 객체 ID를 함께 전달하면, 권한 검사(permission check)와 객체 조회(object lookup) 단계에서 이 데이터가 누구의 것인지에 대해 서로 불일치하게 됩니다. 제가 확인한 가장 치명적인 두 가지 클래스(class)는 임베딩 벡터(embedding-vector) 읽기(소스 텍스트를 부분적으로 재구성할 수 있는 원시 RAG 벡터)와, 피해자의 저장된 OAuth 토큰을 사용하여 다른 테넌트(tenant)의 콘텐츠를 유출하는 커넥터 재색인(connector re-index)이었습니다.
이미 수정된 사례들
유지 관리자(maintainer)들이 수정 사항을 배포했기 때문에 5개 제품은 공개되었습니다. 가장 빠르게 움직인 팀들은 찬사를 받을 자격이 있습니다.
| 도구 (Tool) | 이슈 (클래스) | 상태 |
|---|---|---|
| SurfSense | 저장된 자격 증명을 사용하여 다른 테넌트의 GitHub/Notion 콘텐츠를 유출하는 테넌트 간 커넥터 재색인 | ✅ 수정됨: PR #1503 병합됨 |
| ... |
통과한 사례들
많은 제품이 방어에 성공했습니다. 공로를 인정해 줄 가치가 있는 몇몇 제품의 이름을 언급하겠습니다. 몇몇 제품은 단 한 건의 보고 이후 해당 버그 클래스 전체를 명확하게 감사(audit)했으며, 이는 올바른 대응 방식입니다.
- 현재 릴리스에서 격리(isolation) 유지: Open WebUI, Langfuse, LibreChat, Outline, PraisonAI, Onyx, LangWatch, Khoj. 일부 제품은 이전 버전에서 테넌트 간 CVE가 있었으나, 현재 릴리스에서는 깨끗합니다.
- 오픈 소스 에디션에서 멀티 테넌트(multi-tenant)가 아님 (유출될 수 있는 테넌트 간 접점이 없음): Flowise, Mem0, MaxKB, vLLM. 이들의 워크스페이스 격리는 엔터프라이즈(enterprise) 기능이거나 명시적으로 문서화된 '설계상 공유(shared-by-design)' 동작입니다. 실제로 존재하지 않는 벽이 있다고 가정하고 셀프 호스팅(self-host)을 할 경우 이를 알고 있는 것이 중요합니다.
AI 기반 서비스를 구축할 때의 의미
가장 빈번하게 발생한 순서대로 세 가지 시사점을 정리합니다.
-
쓰기 권한을 승인하듯 읽기 권한도 승인하십시오. list/get/search 엔드포인트는 delete보다 "위험도가 낮은" 것이 아닙니다. 이는 데이터 유출 (exfiltration) 엔드포인트입니다. 소유권 확인 (ownership check) 로직을 추가할 때는, 동일한 객체에 접근하는 모든 형제 (sibling) 엔드포인트를 검색하여 동일한 확인 절차를 적용하십시오. 더 좋은 방법은 개별 핸들러 (handler)가 잊어버릴 수 없는 계층에서 테넌트 스코핑 (tenant scoping)을 강제하는 것입니다. 예를 들어 쿼리 인터셉터 (query interceptor), 행 수준 보안 정책 (row-level policy), 또는 미들웨어 (middleware)를 활용하십시오. 그렇게 하면 격리 (isolation)가 각 엔드포인트가 선택적으로 적용해야 하는 사항이 아니라 기본값 (default)이 됩니다.
-
임베딩 (embeddings)과 캐시 키 (cache keys)를 테넌트 데이터로 취급하십시오. 이번 유출 사례 중 일부는 문서 자체가 아니었습니다. 테넌트 정보가 키에 포함되지 않은 RAG 벡터 (RAG vectors)와 응답 캐시 (response caches)였습니다. 만약 A와 B가 동일한 캐시 버킷 (cache bucket)에 도달하거나 서로의 벡터를 읽을 수 있다면, 그것은 데이터 유출입니다.
-
셀프 호스팅 (self-host)을 한다면, 가정하고 있는 격리 수준을 검증하십시오. 몇몇 인기 있는 도구들은 오픈 소스 버전에서 단일 테넌트 (single-tenant) 방식이거나 설계상 공유 (shared) 방식으로 되어 있습니다. 이는 괜찮은 설계입니다. 하지만 여러 고객을 하나의 인스턴스 (instance)에 배치하면서 격리벽이 있을 것이라 기대한다면, 그 벽이 실제로 존재하는지 확인하십시오.
공개에 관하여
이곳의 모든 내용은 합성 계정 (synthetic accounts)과 합성 데이터 (synthetic data)를 사용하여 격리된 셀프 호스팅 인스턴스에서 테스트되었습니다. 제3자 시스템이나 운영 (production) 시스템에는 아무것도 접촉하지 않았습니다. 모든 발견 사항은 먼저 유지 관리자 (maintainer)에게 비공개로 전달되었으며, 90일간의 유예 기간과 수정안 작성을 제안했습니다. 여기에 언급된 소수의 사례는 이미 수정되었거나 수정 PR (fix PR)이 열려 있기 때문에 공개되었습니다. 나머지는 유지 관리자가 패치를 배포할 때까지 익명으로 유지됩니다. 전체 최신 목록은 sectum.ai/research에서 최신 상태로 유지되며, 각 권고 사항(advisory)이 발행되는 대로 게시됩니다.
만약 아직 공개 유예 (embargo) 상태인 제품의 유지 관리자이며, 공개 시점 조율이나 CVE 발급을 원하신다면 sectum.ai/research를 통해 저에게 연락해 주십시오.
직접 시도해 보기
제가 실행한 점검들은 오픈 소스 (Open Source)입니다. Sectum AI는 두 개의 테넌트 (Tenant)를 구축하고, 테넌트 간 공격 세트 (IDOR/BOLA, RAG 엔티티 유출 (entity-bleed), 캐시 오염 (cache contamination), 삭제 (erasure))를 실행하며, 격리가 작동한다는 벤더 (Vendor)의 말을 신뢰할 필요 없이 감사인 (Auditor)에게 전달할 수 있는 서명된 증거 팩 (evidence pack)을 생성합니다. 만약 멀티 테넌트 (Multi-tenant) AI를 출시하고 있다면, 다른 누군가가 생산 환경 (Production)에 덜 우호적인 무언가를 겨냥하기 전에 스테이징 인스턴스 (Staging instance)를 대상으로 이를 실행해 보십시오.
_ Sectum AI를 통해 발견 및 검증되었습니다. 각 권고 사항 (Advisory)이 게시될 때마다 업데이트되는 전체 최신 공개 목록은 sectum.ai/research에서 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기