SSTI 취약점이란? 서버 템플릿 인젝션의 원리와 방어 방법

SSTI 취약점이 발생하는 원인과 사용자 입력이 서버 템플릿 표현식으로 실행되는 과정, 취약한 Java 코드 및 안전한 방어 방법을 알아봅니다.

웹 애플리케이션은 사용자마다 다른 화면을 만들기 위해 템플릿 엔진을 사용한다. 같은 HTML 문서에 로그인한 사용자의 이름, 상품 정보, 주문 내역과 같은 데이터를 넣어 완성된 페이지를 만드는 방식이다.

템플릿 엔진은 동적인 페이지를 효율적으로 개발할 수 있게 해주지만, 사용자가 입력한 문자열을 템플릿 자체로 처리하면 보안 문제가 발생할 수 있다. 외부 입력값에 포함된 표현식을 서버가 해석하고 실행하는 취약점을 SSTI 취약점이라고 한다.

SSTI 취약점이란?

SSTI는 ‘Server-Side Template Injection’의 약자로, 서버 측 템플릿 인젝션을 의미한다. 사용자 입력값이 단순한 데이터가 아니라 템플릿 문법이나 표현식으로 해석될 때 발생한다.

정상적인 템플릿에서는 개발자가 문서 구조와 표현식을 작성하고, 사용자가 입력한 값은 변수에 담겨 화면에 출력된다. 반면 취약한 애플리케이션은 사용자가 입력한 문자열 전체를 템플릿으로 생성하거나 평가한다.

이 경우 공격자는 템플릿 엔진이 사용하는 표현식 문법을 입력해 서버 내부의 객체와 변수에 접근하려고 시도할 수 있다. 사용하는 템플릿 엔진과 설정에 따라 파일 열람, 내부 정보 노출 또는 코드 실행으로 피해가 확대될 가능성도 있다.

SSTI 취약점의 핵심은 외부 입력값이 템플릿의 데이터가 아니라 실행할 템플릿 코드로 처리된다는 점이다.

서버 템플릿 엔진은 어떻게 동작할까?

서버 템플릿은 미리 작성된 HTML 문서에 동적인 데이터를 결합한다. 예를 들어 템플릿 파일에 사용자 이름을 표시할 위치를 지정해 두고, 서버가 name이라는 변수에 실제 이름을 넣어 최종 HTML을 생성할 수 있다.

이때 템플릿 파일은 개발자가 관리하는 신뢰할 수 있는 코드이고 사용자 이름은 단순한 데이터여야 한다. 사용자가 템플릿의 구조나 실행할 표현식을 직접 결정해서는 안 된다.

문제는 사용자에게 이메일 문구, 알림 메시지 또는 페이지 내용을 자유롭게 작성하게 한 뒤 그 문자열을 템플릿 엔진에 다시 전달하는 경우다. 개발자는 문구 안에 변수를 넣을 수 있도록 만든 기능이라고 생각할 수 있지만, 템플릿 엔진은 사용자 입력에 포함된 표현식도 정상적인 코드로 해석할 수 있다.

SSTI 취약점이 발생하는 원인

SSTI는 외부 입력값을 이용해 새로운 템플릿을 만들거나 템플릿 표현식을 동적으로 평가할 때 주로 발생한다. 사용자 입력값을 기존 템플릿의 변수로 전달하는 방식과 입력값 자체를 템플릿으로 처리하는 방식은 반드시 구분해야 한다.

템플릿 이름이나 템플릿 파일 경로를 사용자가 직접 선택하게 만드는 구조도 주의해야 한다. 검증이 부족하면 의도하지 않은 템플릿이 실행되거나 경로 조작 문제와 결합될 수 있다.

템플릿 엔진이 서버 객체, 클래스 또는 메서드에 광범위하게 접근할 수 있도록 설정된 경우에는 피해가 커질 수 있다. 업무에 필요하지 않은 기능이 활성화돼 있거나 템플릿에 너무 많은 객체를 전달하는 것도 위험 요인이다.

사용 중인 템플릿 엔진과 프레임워크의 버전이 오래된 경우 이미 알려진 우회 방법이나 보안 문제가 남아 있을 수 있다. 따라서 코드 구조뿐 아니라 라이브러리 설정과 버전도 함께 확인해야 한다.

SSTI에 취약한 Java 코드

다음은 사용자가 입력한 문자열을 FreeMarker 템플릿으로 생성해 처리하는 Java 코드다.

@PostMapping("/message/preview")
public String preview(
        @RequestParam String message)
        throws Exception {

    Configuration config =
            new Configuration(
                Configuration.VERSION_2_3_32
            );

    Template template =
            new Template(
                "userTemplate",
                message,
                config
            );

    StringWriter writer = new StringWriter();

    template.process(
        Map.of("siteName", "Security Blog"),
        writer
    );

    return writer.toString();
}

이 코드는 message 값을 템플릿의 변수로 전달하지 않는다. 사용자가 입력한 문자열 자체를 new Template()에 넘겨 새로운 템플릿으로 만든다.

일반 문장만 입력하면 화면에 그대로 표시되지만, 템플릿 표현식이 포함돼 있으면 서버는 이를 템플릿 문법으로 해석한다. 그 결과 사용자가 의도하지 않은 변수나 객체에 접근할 가능성이 생긴다.

사용자 입력을 HTML 이스케이프 처리하더라도 근본적인 문제가 해결되지 않을 수 있다. 템플릿으로 컴파일되기 전에 표현식이 해석된다면 출력 단계의 HTML 이스케이프보다 먼저 위험한 동작이 발생할 수 있기 때문이다.

템플릿 인젝션이 실행되는 원리

공격자는 먼저 입력한 표현식이 단순한 문자로 표시되는지, 아니면 서버에서 계산된 결과로 나타나는지 확인하려고 시도한다. 입력값이 템플릿 엔진에서 해석된다는 사실이 확인되면 사용 중인 엔진과 접근 가능한 객체를 추측할 수 있다.

템플릿 엔진에는 조건문과 반복문, 속성 조회처럼 동적인 문서를 만들기 위한 여러 기능이 있다. 설정이 과도하게 허용돼 있다면 객체의 메서드나 클래스 정보까지 접근할 수 있는 경우도 있다.

공격자는 이러한 기능을 연결해 애플리케이션 설정값이나 환경 정보를 조회하려고 할 수 있다. 서버 내부의 클래스와 메서드에 접근할 수 있는 환경이라면 파일 열람이나 시스템 명령 실행으로 이어질 가능성도 있다.

모든 SSTI 취약점이 원격 코드 실행으로 이어지는 것은 아니다. 실제 영향은 템플릿 엔진의 종류와 보안 설정, 전달된 객체 및 서버 계정의 권한에 따라 달라진다. 그러나 단순한 정보 노출에서 서버 침해로 확대될 수 있으므로 높은 위험도로 다뤄야 한다.

SSTI와 XSS의 차이점

SSTI와 XSS는 모두 입력값이 코드처럼 해석되는 문제지만 실행되는 위치가 다르다.

XSS는 사용자가 입력한 스크립트가 다른 사용자의 브라우저에서 실행되는 취약점이다. 반면 SSTI는 사용자 입력에 포함된 템플릿 표현식이 웹 애플리케이션 서버에서 처리된다.

SSTI 결과로 생성된 HTML에 악성 스크립트가 포함되면 XSS까지 함께 발생할 수 있다. 따라서 템플릿 인젝션을 막는 것과 최종 출력값을 HTML 문맥에 맞게 인코딩하는 작업을 모두 적용해야 한다.

사용자 입력을 데이터로 처리하는 안전한 코드

가장 중요한 대응 방법은 사용자가 전달한 문자열로 템플릿을 생성하지 않는 것이다. 템플릿 파일은 개발자가 작성하고 서버에 고정된 상태로 관리해야 한다. 사용자 입력값은 템플릿에 전달되는 데이터 모델에만 포함한다.

@PostMapping("/message/preview")
public String preview(
        @RequestParam String message)
        throws Exception {

    if (message == null ||
            message.length() > 500) {
        throw new IllegalArgumentException(
            "허용되지 않은 메시지입니다."
        );
    }

    Configuration config =
            new Configuration(
                Configuration.VERSION_2_3_32
            );

    config.setClassLoaderForTemplateLoading(
        getClass().getClassLoader(),
        "/templates"
    );

    config.setDefaultEncoding("UTF-8");
    config.setOutputFormat(
        HTMLOutputFormat.INSTANCE
    );

    config.setAutoEscapingPolicy(
        Configuration
            .ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY
    );

    Template template =
            config.getTemplate(
                "message.ftlh"
            );

    Map<String, Object> model =
            Map.of("message", message);

    StringWriter writer =
            new StringWriter();

    template.process(model, writer);

    return writer.toString();
}

이 코드는 사용자가 입력한 message로 템플릿을 새로 만들지 않는다. 개발자가 작성한 message.ftlh 파일을 고정해서 사용하고, 입력값은 message 변수에 데이터로만 전달한다.

템플릿 파일은 다음과 같이 작성할 수 있다.

<p>${message}</p>

HTML 자동 이스케이프가 활성화돼 있으면 입력값에 HTML 태그가 포함돼도 브라우저에서 코드로 실행되지 않고 문자로 표시될 수 있다.

여기서 중요한 점은 자동 이스케이프가 SSTI 자체를 막는 기능은 아니라는 것이다. 사용자 입력을 템플릿 소스로 사용하지 않는 구조가 먼저 적용돼야 하며, 자동 이스케이프는 최종 출력 과정에서 XSS 위험을 줄이는 추가 대책이다.

템플릿에 전달하는 객체를 최소화해야 한다

템플릿에서 필요하지 않은 객체를 데이터 모델에 넣어서는 안 된다. 요청 객체, 애플리케이션 컨텍스트, 데이터베이스 접근 객체와 시스템 관련 객체를 템플릿에 전달하면 공격이 발생했을 때 접근할 수 있는 범위가 넓어진다.

화면 출력에 필요한 문자열, 숫자와 단순한 DTO만 전달하는 것이 좋다. DTO에서도 공개할 필요가 없는 메서드나 속성이 템플릿을 통해 노출되지 않는지 확인해야 한다.

템플릿 엔진에서 클래스 조회, 임의 메서드 호출 또는 객체 생성 기능을 제한할 수 있다면 업무상 필요한 범위만 허용한다. 사용자가 템플릿을 직접 작성해야 하는 서비스라면 일반 서비스와 분리된 환경에서 제한된 문법만 제공하고, 파일 시스템과 네트워크 접근을 차단해야 한다.

템플릿 이름과 경로를 고정해야 한다

URL 파라미터나 요청값으로 템플릿 파일명을 직접 받아서는 안 된다. 사용자가 전달한 값을 이용해 getTemplate()의 파일명을 구성하면 SSTI뿐 아니라 경로 조작 취약점이 함께 발생할 수 있다.

여러 템플릿 중 하나를 선택해야 한다면 서버에서 허용 목록을 만들어 식별자와 실제 파일명을 연결하는 방식이 안전하다. 사용자는 noticewelcome 같은 식별자만 전달하고, 실제 파일 경로는 서버가 결정해야 한다.

템플릿을 관리자가 수정할 수 있는 기능도 권한을 엄격하게 제한해야 한다. 템플릿 편집 권한은 일반 게시물 작성 권한과 다르게 취급하고, 변경 기록과 승인 절차를 남기는 것이 좋다.

라이브러리 업데이트와 최소 권한

FreeMarker, Thymeleaf, Velocity, Jinja2처럼 서버 템플릿을 처리하는 라이브러리는 최신 보안 버전으로 유지해야 한다. 프레임워크가 기본으로 제공하는 안전한 설정을 임의로 해제하지 않는 것도 중요하다.

웹 애플리케이션은 최소 권한의 전용 계정으로 실행해야 한다. 템플릿 처리 과정에서 예상하지 못한 동작이 발생하더라도 중요 파일과 시스템 명령에 접근하지 못하도록 제한해야 한다.

서버에서 불필요한 외부 통신과 내부 관리망 접근도 차단한다. SSTI가 다른 취약점과 결합됐을 때 추가 파일 다운로드나 내부 시스템 접근으로 피해가 확대되는 것을 줄일 수 있다.

SSTI 취약점 점검 항목

소스코드 점검에서는 사용자 입력값으로 템플릿 객체를 새로 생성하거나 문자열 표현식을 평가하는 코드를 찾아야 한다. new Template(), 표현식 평가 함수와 동적 템플릿 컴파일 기능에 외부 입력값이 전달되는지 확인한다.

getTemplate()에 전달되는 템플릿 이름이 사용자 입력값으로 구성되는지도 살펴봐야 한다. 템플릿 파일은 고정돼 있더라도 사용자가 경로나 이름을 결정할 수 있다면 추가 취약점이 발생할 수 있다.

템플릿 데이터 모델에 어떤 객체가 들어가는지 확인하고, 업무에 필요하지 않은 서버 객체가 노출돼 있지 않은지 점검한다. 자동 이스케이프 설정과 템플릿 엔진의 클래스 및 메서드 접근 제한도 확인해야 한다.

운영 환경에서는 템플릿 구문 오류, 평소 사용하지 않는 표현식 문자열과 비정상적인 서버 프로세스 실행 기록을 모니터링하는 것이 좋다.

자주 묻는 질문

HTML 특수문자를 변환하면 SSTI를 막을 수 있나요?

HTML 이스케이프는 XSS를 줄이는 데 효과적이지만 SSTI의 근본적인 대응 방법은 아니다. 사용자 입력이 템플릿 소스로 처리된다면 서버에서 표현식이 먼저 해석될 수 있다. 사용자 입력을 템플릿이 아닌 데이터로 전달해야 한다.

템플릿 표현식 기호만 차단하면 안전한가요?

충분하지 않다. 템플릿 엔진마다 문법이 다르고 필터를 우회할 수 있는 표현이 존재할 수 있다. 특정 문자를 차단하기보다 사용자가 입력한 문자열을 템플릿으로 평가하지 않는 구조로 변경해야 한다.

SSTI는 항상 서버 명령 실행으로 이어지나요?

항상 그런 것은 아니다. 템플릿 엔진과 보안 설정에 따라 단순한 계산 결과나 제한된 변수만 확인될 수도 있다. 하지만 서버 객체와 메서드에 접근할 수 있는 환경에서는 파일 노출이나 코드 실행으로 확대될 수 있다.

사용자가 템플릿을 편집해야 하는 서비스는 어떻게 해야 하나요?

허용할 변수와 문법을 최소화하고 위험한 클래스와 메서드 접근을 차단해야 한다. 가능하면 범용 템플릿 엔진 대신 기능이 제한된 전용 문법을 제공하고, 별도의 격리 환경에서 렌더링하는 것이 안전하다.

마무리

SSTI 취약점은 외부 입력값이 서버 템플릿의 데이터가 아니라 실행할 표현식으로 해석될 때 발생한다. 공격자는 템플릿 엔진의 기능을 이용해 서버 객체와 내부 정보에 접근하려고 시도할 수 있다.

가장 중요한 대응 방법은 템플릿 파일을 개발자가 관리하는 고정된 코드로 유지하는 것이다. 사용자가 입력한 값은 템플릿에 전달되는 단순한 데이터로만 처리해야 한다.

SSTI 방어의 핵심은 사용자가 서버에서 실행할 템플릿 문법을 결정하지 못하게 하는 것이다. 고정된 템플릿, 최소화된 데이터 모델, 안전한 엔진 설정과 최소 권한을 함께 적용해야 서버 템플릿 인젝션을 효과적으로 예방할 수 있다.

핵심 키워드: SSTI 취약점, 서버 템플릿 인젝션, Server-Side Template Injection, SSTI 공격 원리, 템플릿 엔진 보안, FreeMarker 취약점, SSTI 방어 방법, 템플릿 표현식

댓글 남기기