FlawOpen

flawopen.com/Vulnerabilities/Server-Side Template Injection (SSTI)

Server-Side Template Injection (SSTI): 원인과 수정

Impact depends on reachable template behavior CWE-1336 Template Engine Security
💡 쉽게 설명 — 데이터와 프린터 지시문

“안녕하세요, {{ name }}”이라는 편지 서식으로 인쇄하는 회사를 떠올려 보세요. 안전한 방식은 고정된 서식과 고객 이름을 데이터로 따로 프린터에 전달합니다. 위험한 방식은 인쇄 전에 고객의 텍스트를 서식 지시문에 끼워 넣습니다. 엔진이 그 텍스트를 템플릿 문법으로 해석하면 Server-Side Template Injection이 발생합니다. 영향은 엔진, 설정, 노출된 객체, 프로세스 권한에 따라 달라집니다. 표현식이 계산된다는 사실만으로 운영체제 명령 실행이 증명되지는 않습니다.

대상: Jinja, Twig, FreeMarker, ERB 등의 템플릿 소스 구성과 서버 측 렌더링.
취약 경계: 신뢰할 수 없는 입력을 신뢰된 템플릿의 값으로 전달하는 대신 템플릿 소스로 컴파일하거나 파싱합니다.
영향: 템플릿 표현식 평가. 추가 영향은 엔진 기능, 노출 객체, 샌드박스 및 프로세스 권한에 좌우됩니다.
수정: 템플릿 소스를 신뢰할 수 있는 정적 내용으로 유지하고, 입력은 컨텍스트 데이터로만 전달합니다. 사용자 작성 템플릿이 필요하면 렌더링을 격리합니다.

핵심 개념과 하위 시스템 용어

템플릿 엔진
Jinja나 Twig처럼 템플릿 문법을 파싱하고 템플릿과 컨텍스트 데이터로 출력을 만드는 서버 구성 요소입니다.
템플릿 소스 인젝션
외부 텍스트가 파싱 전에 소스에 포함되어 일반 데이터가 아닌 템플릿 문법으로 해석되는 결함입니다.
컨텍스트 데이터
고정된 신뢰 템플릿에 별도로 전달하는 값입니다. 올바르게 분리하면 {{ 7 * 7 }} 같은 값을 소스로 다시 파싱하지 않습니다.
CWE-1336
템플릿 엔진에서 사용하는 특수 요소의 부적절한 중화입니다. 외부 입력을 템플릿 문법으로 안전하지 않게 처리하는 문제를 설명합니다.

단계별 공격 흐름

Step 1

공격자가 제어하는 입력이 템플릿 경로에 도달

폼 필드, 저장된 프로필 값 등 외부 영향을 받은 문자열이 템플릿을 만드는 코드에 도달합니다. 취약점 경계는 일반적인 값 렌더링이 아니라 소스 구성입니다.

Step 2

애플리케이션이 데이터와 템플릿 지시문을 섞음

정적 템플릿과 별도 컨텍스트 값을 사용하지 않고, 신뢰할 수 없는 문자열을 템플릿 소스에 삽입합니다(예: f-string을 render_template_string에 전달).

Step 3

엔진이 결합된 소스를 파싱

엔진은 언어와 설정에 따라 표현식을 해석합니다. 무해한 산술식으로 평가 여부를 확인할 수 있지만, 그것만으로 파일, 비밀 정보 또는 시스템 명령 접근이 증명되지는 않습니다.

Step 4

영향은 엔진과 배포 환경에 따라 달라짐

추가 영향은 접근 가능한 객체와 함수, 샌드박스 동작, 엔진 제한 및 웹 프로세스 권한에 좌우됩니다. 일부 설정에서는 RCE가 가능하지만 모든 SSTI의 자동 결과는 아닙니다.

메커니즘과 근본 원인

SSTI는 외부 영향을 받은 텍스트를 파싱 전에 템플릿 프로그램의 일부로 만들 때 발생합니다. Flask/Jinja에서 취약한 패턴은 요청 데이터로 새 소스 문자열을 만들어 render_template_string()에 전달하는 것입니다. HTML 이스케이프는 출력 컨텍스트를 보호할 뿐, 신뢰할 수 없는 템플릿 소스를 데이터로 바꾸지 않습니다. 수정 방법은 고정 템플릿을 불러오고 값을 별도 컨텍스트로 전달하는 것입니다. 사용자가 템플릿을 작성해야 한다면 최소한의 데이터만 제공하고 리소스 제한이 있는 격리 worker에서 렌더링하세요. 샌드박스는 심층 방어 수단이지 비밀이나 강력한 애플리케이션 객체를 노출할 이유가 아닙니다.

flask_app.py(취약한 패턴)취약함
# 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)
flask_app.py(보호된 패턴)보호됨
# 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>

공격 순서와 개념 예시

엔지니어링 방어 및 예방 규칙

주요 출처 및 관련 보안 주제:전체 보안 디렉터리 →
CWE-1336: 템플릿 엔진 인젝션 → OWASP WSTG: Server-Side Template Injection → Jinja: 샌드박스와 보안 고려사항 → Jinja API: 템플릿과 loader → Flask: 템플릿 렌더링 →