CVE-2026-33729

CVE-2026-33729

Area
Web / AppSec
Credit
Public Attribution
Credit Note
CVSS
5.8
CVSS Source
CNA / Vendor
CVSS Vector
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:H
CWE
CWE-20 · CWE-345 · CWE-1289
Impact
Authorization Bypass
Key Finding
서로 다른 authorization Check 요청이 같은 cache key로 합쳐져 이전 allow 결과가 deny 요청에 재사용될 수 있었습니다.
Published
Mar 27, 2026
Score Note
Severity
Medium
Status
Public
Target
OpenFGA · Check cache key
조건 context와 store가 다른 요청이 같은 문자열 캐시 키로 합쳐졌습니다. 원래 판정은 달랐지만 먼저 저장된 allow가 다음 deny 요청에 재사용되는 과정을 최소 resolver로 검증했습니다.
Taegu Ha · OpenFGA · CWE-20 · CVSS 5.8
2026년 3월 27일 공개 · OpenFGA v1.13.1에서 수정 · 공식 기록

한눈에 보기

🔎
OpenFGA Check cache key가 서로 다른 security context를 명확히 구분하지 못해, 먼저 cache된 allow 결과가 나중의 deny 요청에 재사용될 수 있었습니다.
구분
확인 내용
프로젝트·컴포넌트
OpenFGA authorization Check cache
공격 입력
서로 다른 context·store를 가진 두 Check request
필요 조건
cache key에서 보안 구분자가 빠지거나 경계가 모호한 조합이 만들어져야 함
취약 지점
request의 모든 authorization discriminator를 담지 않은 ambiguous key material
검증 결과
첫 allow 결과가 다른 deny 요청에서 cache hit로 재사용됨
영향
authorization decision confusion 및 access-control bypass
수정 원칙
모든 보안 구분자를 길이·경계가 명확한 canonical key에 포함

공격 흐름

검증 결론: 원래 deny여야 할 요청이 이전 allow cache entry를 재사용해 허용되는 cross-context authorization bypass를 Go PoC로 고정했습니다.

취약점 개요

OpenFGA는 authorization Check 결과를 캐시해 반복 평가 비용을 줄입니다. 캐시는 요청의 보안 의미를 완전히 구분하는 키를 사용해야 합니다. 조사한 버전에서는 context를 구분자 기반 문자열로 직렬화하고 store 경계를 충분히 포함하지 않아 서로 다른 요청이 같은 키를 만들 수 있었습니다.
캐시가 없을 때 하나는 allow, 다른 하나는 deny를 반환하는 두 요청을 만들었습니다. 첫 번째 allow를 캐시에 넣은 뒤 두 번째 요청을 보내자 resolver가 다시 계산되지 않고 같은 key의 allow가 반환됐습니다.
공격자가 제어하는 값
Check 요청의 condition context와 tuple
취약 지점
BuildCacheKey()의 요청 직렬화
필요 조건
condition 기반 모델과 Check 캐시 활성화
영향
다른 요청의 allow 결과 재사용

조사 대상

인가 시스템의 cache는 성능 기능이지만, 키가 보안 결정의 모든 입력을 1:1로 표현하지 않으면 별도의 권한 경계가 됩니다. 그래서 hash 함수보다 hash 이전의 key material을 먼저 확인했습니다.
map context를 단순 연결한 문자열에서는 값 안에 delimiter와 field 표기 문자를 넣어 두 필드 구조를 한 필드 값처럼 만들 수 있었습니다. 별도로 store ID가 key에서 빠지는 경우도 같은 tuple과 model이 tenant 경계를 넘어 충돌했습니다.
원래 판정이 다른 두 요청이 같은 캐시 키를 갖고, 먼저 계산된 결과가 두 번째 요청에 재사용되는가?

원인 분석

PoC의 fake resolver는 context가 {a: "x,'b:'y"}이면 allow하고 {a: "x", b: "y"}이면 deny하도록 만들었습니다. 구조는 다르지만 취약한 문자열 직렬화 결과는 같았습니다.
두 store 요청도 같은 tuple과 model을 사용하되 store:one은 allow, store:two는 deny로 정했습니다. cache key가 store를 구분하지 않으면 tenant 간에도 앞선 결과가 오염될 수 있음을 별도 단계로 확인했습니다.
poc_check_cache_collision.go · 검증 구조
keyA := graph.BuildCacheKey(*reqA) keyB := graph.BuildCacheKey(*reqB) assert(keyA == keyB) // cache 없이: A=true, B=false cache.ResolveCheck(reqA) // prime allow cache.ResolveCheck(reqB) // poisoned hit -> allow
입력에서 영향까지의 경로
  1. 공격자가 조건 context를 포함한 Check를 보냅니다.
  1. 서로 다른 구조가 같은 key 문자열로 직렬화됩니다.
  1. 첫 요청의 allow 결과가 해당 key에 저장됩니다.
  1. 원래 deny인 두 번째 요청이 같은 key를 조회합니다.
  1. resolver 재평가 없이 allow가 반환됩니다.

재현 및 검증

전체 서버와 데이터베이스를 띄우기 전에 CachedCheckResolver와 실제 BuildCacheKey()를 직접 호출하는 Go PoC를 만들었습니다. backend 판정을 명확히 구분하는 fake resolver를 delegate로 연결했습니다.
cache off baseline, cache prime, poisoned lookup 순서로 결과를 남겼습니다. 이렇게 해야 모델 자체가 두 요청을 동일하게 판단한 것이 아니라 캐시가 결과를 바꿨음을 분리할 수 있습니다.
$ go run .scratch/poc_check_cache_collision.go
검사
입력·조건
관찰
의미
Context baseline
A와 B, cache 없음
A=true, B=false
두 요청의 원 판정이 다름
Context key
A와 B key 비교
same_key: true
구조가 다른 context 충돌
Context cache
A로 prime 후 B
B=true
allow 결과 오염
Store 경계
store one/two
같은 key와 allow 재사용
tenant 식별도 부족
PoC가 고정한 판정 변화
== Same-store context collision == same_key: true baseline(A): true baseline(B): false default(B) after poisoned hit: true == Cross-store cache contamination == same_key: true baseline(store:one): true baseline(store:two): false default(store:two): true PoC status: SUCCESS

판단 변화

Hash 충돌

처음에는 일반적인 hash collision을 의심했지만 문제는 cryptographic hash의 우연한 충돌이 아니었습니다. hash 이전 문자열이 이미 동일했습니다. 따라서 공격 난이도를 무작위 충돌 탐색으로 설명하지 않고 key serialization ambiguity로 좁혔습니다.

모델 판정

두 요청이 같은 결과를 내는 모델이라면 key가 같아도 보안 영향이 보이지 않습니다. fake resolver에서 baseline을 일부러 반대로 만들고 cache를 켠 뒤에만 결과가 뒤집히도록 구성해 cache와 authorization logic을 분리했습니다.

Store 범위

context delimiter만 고치면 충분하다고 생각할 수 있었지만 store ID가 빠진 별도 경계도 확인했습니다. 수정 기준을 특정 delimiter escape가 아니라 요청의 모든 보안 구분자를 모호하지 않게 포함하는 것으로 바꿨습니다.

영향과 수정

조건 기반 관계와 캐시가 함께 사용될 때 공격자가 이전 요청과 충돌하는 Check를 만들어 잘못된 allow 결과를 받을 수 있습니다. store 경계까지 충돌할 경우 다른 tenant의 cached decision이 섞일 수 있습니다.

확인한 범위

  • 다른 baseline 결과를 가진 요청의 동일 key
  • allow로 prime한 뒤 deny 요청이 allow로 변경
  • context와 store 두 종류의 경계

제외한 범위

  • 캐시가 꺼진 배포
  • 모든 OpenFGA 모델의 무조건적 영향
  • 데이터 저장소 자체의 변조

수정

v1.13.1은 cache key를 구성할 때 store와 condition context를 모호하지 않게 구분하도록 수정했습니다. 구조화된 값은 delimiter 연결이 아니라 경계와 길이가 보존되는 표현으로 key material을 만들어야 합니다.

분석 기준과 증거 보존

항목
최종 감사 결과
소스 기준
OpenFGA checkout 052a6e7aa6b0e13633b2aaf4cd201d16d65cf384
원본 검증
서로 달라야 하는 두 Check request, cache key 충돌과 allow 재사용을 실제 Go PoC로 확인
GitHub 보존 상태
Public 저장소 main의 감사 commit c04cacf; evidence 4개와 root manifest.json에 size·SHA-256 고정
무결성
사례별 SHA256SUMS 검증 통과, root manifest의 모든 file hash와 실제 파일 일치, README 상대 링크 확인 완료
보존 경계·제약
원본 작업 session과 외부 서비스 자격 증명은 제외
감사 완료
2026-08-23 · Docker/로컬 원본 대조 후 GitHub push 완료

관련 파일

2026-08-23 최종 감사

실제 OpenFGA cache resolver를 사용하는 Go PoC와 공개 기록을 포함했습니다.

느낀 점

캐시는 원래 authorization 뒤의 최적화처럼 보였지만 실제로는 이전 보안 결정을 다시 공급하는 또 하나의 결정 계층이었습니다. 이후 authz 코드를 볼 때 evaluator뿐 아니라 cache key가 무엇을 생략하고 어떻게 직렬화하는지도 같은 우선순위로 보게 됐습니다.
PoC에서 baseline과 cached result를 분리한 것이 특히 중요했습니다. 결과가 단순히 true라는 것보다 ‘원래 false였고 cache를 켠 뒤 true로 변했다’는 순서가 원인을 설명했습니다.