flawopen.com/SSTI/FreeMarker와 Velocity
FreeMarker와 Velocity 템플릿은 사용자 입력에 안전한가요?
신뢰할 수 있는 템플릿에 삽입된 사용자 값은 데이터입니다. 사용자가 템플릿 소스를 제어하면 다릅니다. 템플릿 언어 표현식을 작성할 수 있기 때문입니다. 도달 가능한 기능은 버전, wrapper 또는 introspector, 모델 객체, 앱 권한에 따라 달라집니다. 모든 템플릿 주입이 OS 명령 실행으로 이어진다고 단정하지 말고, 신뢰할 수 없는 템플릿 소스를 실행 로직으로 취급해 앱 프로세스 밖에서 렌더링하세요.
// 위험: 요청 데이터를 템플릿 소스로 파싱 String src = "Hello " + userSuppliedName; Template t = new Template("t", src, cfg); t.process(model, writer); // 위험: 사용자 제어 VTL 문자열 평가 Velocity.evaluate(context, writer, "log", userTemplate);
// 신뢰된 classpath 템플릿; 모델은 스칼라만 사용 Configuration cfg = new Configuration( Configuration.VERSION_2_3_34); cfg.setTemplateLoader(new ClassTemplateLoader( App.class, "/templates")); cfg.setNewBuiltinClassResolver( TemplateClassResolver.ALLOWS_NOTHING_RESOLVER); cfg.setObjectWrapper(new SimpleObjectWrapper( Configuration.VERSION_2_3_34)); Template t = cfg.getTemplate("hello.ftl"); Map<String,Object> model = Map.of( "name", userSuppliedName); t.process(model, writer); // Velocity: 신뢰된 템플릿과 제한된 introspection (심층 방어) Properties p = new Properties(); p.setProperty("resource.loaders", "class"); p.setProperty("resource.loader.class.class", ClasspathResourceLoader.class.getName()); p.setProperty("runtime.introspector.uberspect", SecureUberspector.class.getName()); VelocityEngine engine = new VelocityEngine(); engine.init(p); Template vt = engine.getTemplate("hello.vm", "UTF-8"); VelocityContext vc = new VelocityContext(); vc.put("name", userSuppliedName); vt.merge(vc, writer);
Java 엔진이 위험한 기능을 노출할 수 있는 이유
템플릿은 데이터 모델을 대상으로 표현식을 평가하지만 사용 가능한 Java 표면은 구성에 따라 달라집니다. FreeMarker 기본 wrapper는 임의 POJO의 JavaBean 속성과 메서드를 노출할 수 있으며 접근 가능한 멤버는 wrapper와 접근 정책에 달려 있습니다. Velocity 기본 Uberspect는 VTL 속성을 공개 Java 메서드에 연결합니다. 모든 모델이 무제한 reflection을 제공한다는 뜻은 아닙니다. 서비스 객체, class loader, 요청 객체 등 강력한 기능이 모델에 포함되면 위험이 커집니다. 문자열 map과 단순 값을 우선하고 신뢰된 템플릿에 필요한 필드만 제공하세요.
FreeMarker built-in ?new는 TemplateModel 구현 클래스를 인스턴스화할 수 있습니다. Apache는 업로드 템플릿에서 클래스 해석을 제한하라고 권고합니다. SAFER_RESOLVER는 일부 클래스를 허용하고 ALLOWS_NOTHING_RESOLVER는 모든 해석을 거부합니다. 이 설정만으로 제한된 wrapper와 최소 데이터 모델을 대체할 수는 없습니다.
위험을 낮추고 한계를 이해하기
- ✓FreeMarker: 신뢰할 수 없는 템플릿에는
new_builtin_class_resolver를ALLOWS_NOTHING_RESOLVER로 설정하고 임의 POJO를 거부하는SimpleObjectWrapper같은 wrapper를 사용하세요. 목적이 제한된 단순 값만 전달하세요. - ✓Velocity: 신뢰할 수 없는 템플릿에는
SecureUberspector를 사용하고 context에 필요한 단순 값만 넣으세요. 문서화된 reflection 및 객체 생성 경로를 차단하지만 OS sandbox는 아닙니다. - ✓두 엔진 모두: 변조 방지된 신뢰 출처에서 앱 템플릿을 로드하세요. 요청 매개변수나 사용자가 수정 가능한 DB 행을 템플릿 소스로 변환하지 마세요.
- ✓사용자 작성 템플릿이 제품 요구사항이라면: 최소 권한의 별도 프로세스에서 렌더링하고 네트워크, 비밀 정보, 앱 쓰기 가능 파일을 차단하세요. 엔진 밖에서 CPU, 메모리, 시간을 제한하세요.
엔진 제한은 defense-in-depth이지 프로세스 격리가 아닙니다. 지원 버전을 사용하고 공식 최신 지침에 따라 구성을 확인하며 renderer에 비밀 정보나 권한 객체를 주지 마세요.
자주 묻는 질문
관련은 있지만 별개의 위험입니다. 사용자가 제어하는 view 이름만으로 expression injection이 성립하지는 않습니다. Thymeleaf 또는 Spring Expression Language 평가로 이어지는 실제 데이터 흐름을 확인하고 신뢰할 수 없는 입력으로 표현식 조각을 만들지 마세요. 연동 라이브러리를 최신 상태로 유지하고 신뢰된 앱 로직으로 view를 선택하세요.
템플릿 작성자는 엔진과 데이터 모델이 노출한 기능을 사용할 수 있지만, 항상 OS 명령 실행이 가능하다는 뜻은 아닙니다. 템플릿 수정 권한을 민감한 코드 작성 권한으로 보고 신뢰된 유지보수자에게만 주며 변경을 감사하세요.
Mustache 같은 logic-less 엔진은 표현식과 메서드 호출 표면을 줄일 수 있지만 출력 인코딩, 템플릿 출처 통제, helper 기능도 중요합니다. 해당 신뢰 모델에 맞는 엔진을 고르고 렌더링을 격리하세요. 범용 JVM 엔진을 sandbox로 간주하지 마세요.