[Web] Web · 작성: 2026-07-20 03:10:34 · 수정: 2026-07-20 03:11:15 · 조회 10
XSS가 "브라우저가 해석하는 HTML/JS"에 사용자 입력이 섞여드는 문제라면, SSTI는 "서버가 해석하는 템플릿 문법"에 사용자 입력이 섞여드는 문제다. 이 블로그도 EJS로 서버 렌더링을 하는데, 만약 사용자 입력을 템플릿 문자열 자체에 그대로 이어 붙이는 코드가 있었다면 같은 문제가 생겼을 것이다 — 실제로는 항상 값을 변수로 넘겨서 렌더링하기 때문에 안전하고, 이 차이가 SSTI의 핵심이다.
// 취약: 사용자 입력을 템플릿 "문자열" 자체에 이어붙여서 컴파일
const template = "안녕하세요 " + req.query.name;
res.send(ejs.render(template));
// 안전: 템플릿은 고정, 사용자 입력은 "값"으로만 전달
res.render('greet', { name: req.query.name });
첫 번째 방식은 req.query.name에 EJS 문법 자체(예: 셸 명령을 실행하는 구문)를 넣으면 서버에서 그대로 코드로 실행된다.
SSTI는 어떤 템플릿 엔진을 쓰느냐에 따라 문법이 다 다르다.
{{ 7*7 }} → 49가 나오면 SSTI 존재가 확정된다. RCE로는 __init__, __globals__, __builtins__ 같은 파이썬 객체 내부 체인을 타고 들어가 os.popen 같은 함수까지 도달하는 페이로드를 쓴다.<#assign ex="freemarker.template.utility.Execute"?new()> 뒤에 이어서 ex("id")를 호출하는 표현식을 쓴다.<%= %> 태그 안에 process.mainModule.require('child_process')로 시작하는 체인을 넣어 임의 명령을 실행한다.XSS 페이로드로는 안 터지는데 {{7*7}}처럼 엔진별 산술식 문법을 넣었을 때 결과값(49)이 그대로 반영되면 SSTI 후보다. 어떤 문법에 반응하는지로 엔진을 특정하고, 그 엔진 전용 페이로드로 전환한다.
res.render(view, {변수}) 패턴이 바로 이 원칙을 지키는 방식이다