flawopen.com/Vulnerabilities/Server-Side Template Injection (SSTI)
Server-Side Template Injection (SSTI): 원인과 수정
“안녕하세요, {{ name }}”이라는 편지 서식으로 인쇄하는 회사를 떠올려 보세요. 안전한 방식은 고정된 서식과 고객 이름을 데이터로 따로 프린터에 전달합니다. 위험한 방식은 인쇄 전에 고객의 텍스트를 서식 지시문에 끼워 넣습니다. 엔진이 그 텍스트를 템플릿 문법으로 해석하면 Server-Side Template Injection이 발생합니다. 영향은 엔진, 설정, 노출된 객체, 프로세스 권한에 따라 달라집니다. 표현식이 계산된다는 사실만으로 운영체제 명령 실행이 증명되지는 않습니다.
핵심 개념과 하위 시스템 용어
- 템플릿 엔진
- Jinja나 Twig처럼 템플릿 문법을 파싱하고 템플릿과 컨텍스트 데이터로 출력을 만드는 서버 구성 요소입니다.
- 템플릿 소스 인젝션
- 외부 텍스트가 파싱 전에 소스에 포함되어 일반 데이터가 아닌 템플릿 문법으로 해석되는 결함입니다.
- 컨텍스트 데이터
- 고정된 신뢰 템플릿에 별도로 전달하는 값입니다. 올바르게 분리하면
{{ 7 * 7 }}같은 값을 소스로 다시 파싱하지 않습니다. - CWE-1336
- 템플릿 엔진에서 사용하는 특수 요소의 부적절한 중화입니다. 외부 입력을 템플릿 문법으로 안전하지 않게 처리하는 문제를 설명합니다.
단계별 공격 흐름
공격자가 제어하는 입력이 템플릿 경로에 도달
폼 필드, 저장된 프로필 값 등 외부 영향을 받은 문자열이 템플릿을 만드는 코드에 도달합니다. 취약점 경계는 일반적인 값 렌더링이 아니라 소스 구성입니다.
애플리케이션이 데이터와 템플릿 지시문을 섞음
정적 템플릿과 별도 컨텍스트 값을 사용하지 않고, 신뢰할 수 없는 문자열을 템플릿 소스에 삽입합니다(예: f-string을 render_template_string에 전달).
엔진이 결합된 소스를 파싱
엔진은 언어와 설정에 따라 표현식을 해석합니다. 무해한 산술식으로 평가 여부를 확인할 수 있지만, 그것만으로 파일, 비밀 정보 또는 시스템 명령 접근이 증명되지는 않습니다.
영향은 엔진과 배포 환경에 따라 달라짐
추가 영향은 접근 가능한 객체와 함수, 샌드박스 동작, 엔진 제한 및 웹 프로세스 권한에 좌우됩니다. 일부 설정에서는 RCE가 가능하지만 모든 SSTI의 자동 결과는 아닙니다.
메커니즘과 근본 원인
SSTI는 외부 영향을 받은 텍스트를 파싱 전에 템플릿 프로그램의 일부로 만들 때 발생합니다. Flask/Jinja에서 취약한 패턴은 요청 데이터로 새 소스 문자열을 만들어 render_template_string()에 전달하는 것입니다. HTML 이스케이프는 출력 컨텍스트를 보호할 뿐, 신뢰할 수 없는 템플릿 소스를 데이터로 바꾸지 않습니다. 수정 방법은 고정 템플릿을 불러오고 값을 별도 컨텍스트로 전달하는 것입니다. 사용자가 템플릿을 작성해야 한다면 최소한의 데이터만 제공하고 리소스 제한이 있는 격리 worker에서 렌더링하세요. 샌드박스는 심층 방어 수단이지 비밀이나 강력한 애플리케이션 객체를 노출할 이유가 아닙니다.
# VULNERABLE: request data becomes template source before parsing.
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route("/greet")
def greet():
name = request.args.get("name", "Guest")
# A value such as {{ 7 * 7 }} is interpreted by the template engine.
source = f"<h1>Welcome, {name}!</h1>"
return render_template_string(source)
# HARDENED: fixed template source; request data stays a context value.
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route("/greet")
def greet():
name = request.args.get("name", "Guest")
return render_template("greet.html", name=name)
# templates/greet.html is trusted, static source:
# <h1>Welcome, {{ name }}!</h1>
공격 순서와 개념 예시
- 1입력 출처: 외부 영향을 받은 값이 템플릿을 구성하는 경로에 도달합니다.
- 2안전하지 않은 조합: 애플리케이션이 값을 별도 컨텍스트 데이터로 전달하지 않고 템플릿 소스에 넣습니다.
- 3해석: 엔진이 결합된 소스를 파싱합니다. 승인된 테스트 환경에서 무해한 산술식으로 평가를 확인할 수 있습니다.
- 4조건부 영향: 추가 접근은 엔진, 노출 객체, 설정 및 프로세스 권한에 좌우됩니다. 표현식 평가만으로 RCE를 단정하지 마세요.
엔지니어링 방어 및 예방 규칙
- ✓소스를 고정하세요: 신뢰된 템플릿을 파일이나 제어된 loader에서 불러오고 사용자 입력은 컨텍스트 값으로만 전달하세요. 요청 데이터를 소스에 연결하지 마세요.
- ✓SSTI와 HTML 인젝션을 구분하세요: HTML 이스케이프는 출력 컨텍스트를 보호하지만 신뢰할 수 없는 입력이 템플릿 소스로 파싱되는 흐름은 고치지 못합니다.
- ✓사용자 템플릿을 제한하세요: 기능이 필요하다면 해당 용도의 엔진 샌드박스를 쓰고 최소 데이터 모델만 노출하며 리소스 제한이 있는 격리 worker에서 렌더링하세요. 엔진 보안 안내를 따르세요.
- ✓영향 범위를 줄이세요: 렌더러를 비루트 서비스로 실행하고 파일, 네트워크 및 비밀 정보에 대한 접근을 최소화하세요. 이는 주요 수정이 아니라 피해 억제입니다.
- ✓회귀 테스트를 추가하세요:
{{ 7 * 7 }}같은 무해한 표시를 데이터로 전달해 그대로 데이터로 남는지 확인하고, 템플릿이 신뢰된 loader에서만 오는지 검증하세요.