SSTI란?
SSTI(Server-Side Template Injection)란, 웹 템플릿 엔진을 사용하는 웹 서비스에서 공격자가 템플릿 구문을 삽입하여 서버 내에서 악의적인 동작을 수행하도록 하는 공격이다.
웹 템플릿 엔진이란?
웹 템플릿 엔진이란, “지정된 템플릿 양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어다.” 코드와 디자인을 분리하여 개발할 수 있도록 해주는 도구라고도 볼 수 있다.

위 예시처럼 고정적으로 사용하는 html 문서를 템플릿으로 작성해 두고 동적으로 변경되는 부분만 데이터와 함께 템플릿 엔진에 전달하면, 템플릿 엔진이 이를 합쳐 최종적으로 html 문서를 생성하여 보여주는 것이다. 이렇게 되면 웹 문서를 작성할 때마다 타이틀이나 네비게이션 바 등을 반복적으로 구현할 필요가 없어지게 되고, 코드의 복잡성이 낮아져 소스 코드를 직관적으로 확인할 수 있게 된다.
최근 개발된 웹 페이지의 다수는 이러한 이유로 웹 템플릿 엔진을 사용하여 개발된다.
이때 사용자의 입력이 템플릿에 직접 삽입되고, 이 사용자의 입력이 템플릿 구문이라 템플릿 엔진에서 렌더링되는 과정에서 템플릿 구문이 동작한다면 의도하지 않은 공격자의 악의적인 동작이 수행될 수 있다. 이처럼 SSTI는 공격자가 템플릿에 템플릿 구문을 삽입할 수 있을 때 발생하는 공격이다.
SSTI 발생 예시
$output = $twig->render("Dear {first_name},", array("first_name" => $user.first_name) );
위 코드는 PHP에서 사용하는 twig라는 템플릿 엔진을 사용한 예시다. Dear {first_name}이 템플릿에 해당하는 부분이고, {first_name} 부분이 템플릿 구문으로, 렌더링 과정을 거치면서 first_name 자리에 $user.first_name의 값이 들어가게 된다.
문제는 이 템플릿이 사용자 입력의 영향을 받을 때 발생한다.
$output = $twig->render($_GET['custom_email'], array("first_name" => $user.first_name) );
위 코드는 템플릿 자리가 $_GET으로 대체되었다. 이로 인해 사용자는 custom_email이라는 파라미터를 활용해 템플릿을 직접 작성할 수 있는 상태가 되었다. 템플릿 렌더링도 파라미터를 전달받은 후 진행된다.
이때 일반적으로 발생하는 취약점은 XSS이다. 하지만 이때 공격자가 파라미터로 템플릿 구문을 입력하게 될 경우, 사용자의 템플릿 구문이 서버 코드에서 실행되는 SSTI가 발생할 수 있다.
가령, 다음과 같은 입력-출력이 발생할 수 있다.
custom_email={{7*7}}
49
파라미터 custom_email의 값으로 템플릿 구문을 넣어 보낸 예시다. 템플릿 구문 안의 7*7이라는 식이 실제로 실행되어 식 대신 49라는 결과값을 출력한 것이다. 다음 예시도 템플릿 구문을 보냈을 때 발생할 수 있는 결과의 예시다.
custom_email={{self}}
Object of class __TwigTemplate_7ae62e582f8a35e5ea6cc639800ecf15b96c0d6f78db3538221c1145580ca4a5 could not be converted to string
SSTI의 영향
위 예시들을 통해 SSTI가 그렇게 단순한 취약점은 아니라는 것은 알았다. 하지만 공격자로서도, 방어자로서도 알아야 할 건, 그래서 이 취약점으로 어디까지 가능하냐는 것이다.
SSTI의 가능성을 알아보기 전에 먼저 위의 예시들이 어떻게 가능하게 된 것인지부터 알아야 한다.
웹 어플리케이션을 개발할 때 개발자는 페이지의 반복되는 부분을 html로 구현하고, 페이지마다 변하는 부분을 템플릿 구문으로 작성한 ‘템플릿’이라는 것을 만든다. 그리고 템플릿 엔진은 템플릿과 데이터를 받고, 템플릿 구문을 실행하여 최종적인 페이지를 반환한다.
그런데 이 템플릿을 채우기 위해 템플릿 엔진에서 실행시키는 이 템플릿 구문 또한 하나의 코드다.
<dl>
{% for key, value in my_dict.items() %}
<dt>{{ key|e }}</dt>
<dd>{{ value|e }}</dd>
{% endfor %}
</dl>
{% set navigation = [('index.html', 'Index'), ('about.html', 'About')] %}
{% set key, value = call_something() %}
{{ "Hello, {}!".format(name) }}
위 코드는 Jinja2 공식 문서에서 제공하는 템플릿 문법 예시로, “조건문, 변수 할당, 메서드 호출 등 웬만한 프로그래밍에서 지원하는 기능이 모두 사용 가능하다.”
(템플릿 구문은 애초에 목적이 다르기 때문에 문법이 약간 다르지만) 근본적으로 프로그래밍 언어를 사용해 웹개발을 간단하게 만들자고 나온 도구이기 때문에 프로그래밍 언어의 기능이 대부분 지원된다. 가령, Jinja2는 파이썬 기반 엔진이기 때문에 위 예시처럼 기본적인 변수, 객체 호출이나 조건문과 반복문 사용도 가능하고 파이썬 내장 매서드까지 호출할 수 있다. Twig는 PHP 기반 엔진이기 때문에 템플릿 구문 문법에서 PHP와 유사한 성격을 띤다.
그렇기 때문에 Template injection을 허용한다는 건 코드 실행을 허용한다는 것과 동일하다. 따라서 SSTI 취약점이 존재할 때 이 취약점으로 가능한 공격의 범위는 해당 템플릿 엔진의 기반 프로그래밍 언어로 가능한 동작의 범위와 거의 같다고 보면 된다.
Jinja2를 예시로 들면, Flask의 config 속성을 불러올 수 있다.

이때 config 속성의 SECRET_KEY 값은 jwt 토큰 서명에 사용되는 값으로, 세션의 무결성 보장에 있어 중요한 값이다. 이 SECRET_KEY 값이 노출되면 공격자는 서명을 생성할 수 있게 되므로 토큰을 위조할 수 있게 된다.
또는 쉘 명령어를 실행할 수도 있다.

사용된 payload는 {{ ''.__class__.__mro__[1].__subclasses__()[408]('id',shell=True,stdout=-1).communicate() }}이다. ''로 string 타입 object를 만들고, (파이썬에서 호출 가능한) 특수 속성(special attributes)을 활용해 subprocess.Popen을 불러와 쉘 명령어를 실행하는 payload이다. Jinja2를 예시로 들었지만 다른 엔진, 파이썬 기반이 아닌 엔진에서도 쉘 명령어를 실행시키는 방법 역시 존재한다.
정리하면, “SSTI 취약점은 RCE(Remote Code Execution, 원격 코드 실행)까지도 발생할 수 있는 심각한 취약점이라는 것이다.”
SSTI 취약점의 대응
1. 정적 템플릿 사용
SSTI 취약점이 발생하는 가장 근본적인 이유는, 사용자의 값이 템플릿에 포함되어 템플릿 구문으로 동작할 여지가 존재하기 때문이다. 가장 좋은 방법은 사용자의 입력을 템플릿에 포함하지 않는 것이다.
Flask(Jinja2)의 경우, render_template()이라는 함수를 사용할 수 있는데, “이 함수를 사용하면 웹 서버의 templates 디렉토리에 포함된 html 문서에 작성된 템플릿 구문을 제외하고는 전부 문자열로 인식되기 때문에 사용자의 입력값을 템플릿 구문으로 해석할 여지를 없앨 수 있다.”
2. 입력값 검증
각 템플릿 엔진 별로 템플릿 구문에 사용되는 특수문자에 대한 이스케이프 처리를 하는 것이다. 하지만 모든 필터링이 그러하듯, 필터링만으로는 우회가 가능하기 때문에 다른 방어 수단이 같이 구현되어야 한다.
참고자료
[1] 웹 템플릿 엔진 기반의 SSTI 취약점 분석
[2] Portswigger Research - Server-Side Template Injection
https://portswigger.net/research/server-side-template-injection
[3] Portswigger Academy - Server-Side Template Injection
https://portswigger.net/web-security/server-side-template-injection
[4] Jinja2 Template Designer Documentation (Jinja2 공식문서)