
CVE-2025-23211에 대한 개념 증명(PoC)으로, 승인된 보안 검증, 패치 테스트, 완화 확인을 지원하기 위해 데이터 개인정보 보호 관련 취약점을 재현합니다.
원격 코드 실행으로 이어지는 Tandoor Recipes의 서버 측 템플릿 주입(SSTI) 취약점인 CVE-2025-23211을 시연하는 자체 포함형 재현 환경입니다. 이 저장소는 취약한 버전을 구동하여 익스플로잇을 보여준 뒤, 패치된 버전을 구동하여 동일한 입력이 거부되는 모습을 보여줍니다.
| CVE | CVE-2025-23211 |
| 권고 | GHSA-r6rj-h75w-vj8v |
| 분류 | 서버 측 템플릿 주입 (Jinja2), CWE-94 / CWE-1336 |
| CVSS 3.1 | 9.9 (치명적) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| 영향받는 버전 | Tandoor Recipes <= 1.5.23 |
| 수정된 버전 | 1.5.24 |
Tandoor Recipes는 오픈 소스 셀프 호스팅 식사 계획 및 레시피 관리 애플리케이션입니다. 백엔드는 Gunicorn으로 구동되는 Django(Python)이고, 프런트엔드는 Vue.js 단일 페이지 애플리케이션이며, 데이터는 PostgreSQL에 저장됩니다.
Tandoor는 사용자가 레시피의 조리 지시문 텍스트 안에 Jinja2 템플릿 표현식을 작성할 수 있게 해줍니다. 이 기능은 재료 수량 조정과 같은 작업을 위해 의도된 것입니다. 이러한 지시문은 서버에서 렌더링됩니다.
조리 지시문 텍스트는 cookbook/helper/template_helper.py의 render_instructions()에 의해 처리됩니다. 영향을 받는 버전에서는 이 텍스트가 Jinja2의 기본 Template()에 전달되어 샌드박스 없이 렌더링됩니다:
template = Template(instructions)
instructions = template.render(ingredients=ingredients, scale=scale)
기본 Jinja2 환경은 Python의 객체 그래프를 노출하기 때문에, 레시피를 편집할 수 있는 인증된 사용자는 빈 튜플에서 subprocess.Popen으로 이어지는 템플릿 표현식을 주입하여 서버에서 임의의 명령을 실행할 수 있습니다. 기본 컨테이너는 root로 실행되므로 명령도 root 권한으로 실행됩니다.
조리 지시문은 Jinja2가 렌더링하기 전에 Markdown 단계를 거치며, 이 단계에서 원시 밑줄이 제거됩니다. 따라서 익스플로잇은 밑줄을 16진수로 인코딩(\x5f\x5f)하고 점 표기법 대신 attr() 필터를 사용합니다. payloads.txt를 참조하세요.
compose-vulnerable.yml Tandoor 1.5.23 (취약) 및 PostgreSQL
compose-patched.yml Tandoor 1.5.24 (패치됨) 및 PostgreSQL
payloads.txt 두 가지 개념 증명 페이로드와 예상 출력
enumerate-popen.txt 다른 이미지에서 subprocess.Popen 인덱스를 찾기 위한 도우미
이 저장소의 증거는 아래 설정에서 캡처되었습니다. Compose v2 이상이 포함된 최신 Docker 릴리스라면 동일한 결과를 재현할 수 있습니다.
취약한 스택을 시작합니다:
docker compose -f compose-vulnerable.yml up -d
웹 컨테이너 로그에 워커가 부팅되었다는 메시지가 표시될 때까지 기다린 후 http://localhost:8080을 엽니다.
첫 번째 계정을 등록합니다. 첫 번째 사용자가 슈퍼유저가 됩니다. 메시지가 표시되면 Space 이름을 설정합니다.
새 레시피를 만듭니다. Steps > Instructions 필드에 payloads.txt의 Payload 1을 붙여넣고 저장합니다.
레시피를 열어 확인합니다. 렌더링된 조리 지시문에 <class 'subprocess.Popen'>이 표시되며, 주입이 Python 내부까지 도달했음을 확인할 수 있습니다.
Payload 2로 반복합니다. 이제 레시피를 보면 whoami가 실행되어 root가 표시됩니다.
프로세스 사용자를 직접 확인합니다:
docker compose -f compose-vulnerable.yml exec web_recipes whoami
취약한 스택을 중지합니다:
docker compose -f compose-vulnerable.yml down
패치된 스택을 시작합니다:
docker compose -f compose-patched.yml up -d
동일한 페이로드로 동일한 단계를 반복합니다. 실행되는 대신, 이제 레시피 보기에는 Could not parse template code. Error: Undefined Error가 표시됩니다.
모든 것을 초기화하고 빈 데이터베이스에서 시작하려면 docker compose -f compose-vulnerable.yml down -v를 실행하세요. -v 플래그는 명명된 볼륨을 제거하므로, 다음 실행 시 데이터베이스가 처음부터 다시 생성됩니다.
패치(커밋 e6087d5)는 샌드박스가 없는 Template()을 Jinja2의 SandboxedEnvironment로 교체합니다:
env = SandboxedEnvironment()
instructions = env.from_string(instructions).render(ingredients=ingredients, scale=scale)
샌드박스는 Jinja2의 속성 조회를 재정의하여 __class__, __base__, __subclasses__, __getitem__과 같은 안전하지 않은 속성에 대한 접근을 금지합니다. 이러한 속성이 없으면 페이로드는 일반 객체에서 subprocess.Popen으로 이어지는 경로를 찾을 수 없으므로, 표현식은 실행되는 대신 실패합니다.
익스플로잇은 점 표기법이 아닌 attr() 필터를 통해 속성에 접근하므로, 패치된 코드에서는 차단된 접근이 SecurityError가 아닌 UndefinedError로 나타납니다. 두 오류 모두 샌드박스 버전에서 처리됩니다.
이 저장소는 교육 목적으로 공개적으로 공개되고 이미 패치된 취약점을 재현합니다. 여기에 제공된 로컬 컨테이너에 대해서만 실행하세요. 소유하지 않았거나 테스트 권한이 없는 시스템을 대상으로 하지 마세요.