1. 소스코드
@Controller
public class HelloController {
@GetMapping("hello")
public String helloMvc(@RequestParam("name") String name, Model model) {
model.addAttribute("name", name);
return "hello";
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:utext="'반갑습니다. ' + ${name}"></p>
</body>
</html>
2. 서술
Thymeleaf는 기본적으로 th:text 태그를 사용할 경우 입력값에 대하여 HTML Entity 처리가 자동으로 되는 모양이다.
갈수록 공격 벡터가 줄어드는 느낌이다. 하지만 개발자 입장에서도 해당 태그는 상당히 성가실 경우가 많을 것 같다.
Thymeleaf 쪽에서도 이 사실을 인지하고 있는지 th:utext 라는 대안을 만들어두었다.
기존의 소스코드에서 태그하나만 변경(utext)해주면 정상적으로 XSS가 터지게 된다.
<p th:utext="'반갑습니다. ' + ${name}"></p>
3. Thymeleaf 분석
소스코드는 공개되어 있다.
Git : Thymeleaf
3.1 getDisplayString()
Thymeleaf 소스코드를 다운받아서 열어보니 아래와 같은 로직이 존재한다.
해당 getDisplayString 메소드는 그림과 깉이 5군데에서 호출되는 것을 확인할 수 있었다.
3.2 SpringValueFormatter.java - getDisplayString()
여기서 4번째 클래스(SpringValueFormatter.java)로 접근을 시도하면, 상위 클래스로의 접근이 가능하다.
해당 함수 역시 4군데에서 호출되는 것을 확인할 수 있다.
3.3 SpringInputGeneralFieldTagProcessor.java
2번째 클래스(SpringInputGeneralFieldTagProcessor.java) 에 접근 시 익숙한 "text" 태그를 확인할 수 있다.
해당 클래스는 Input타입의 태그들을 제어하는 클래스로 보이며, 태그들의 리스트는 다음과 같다.
실제로 몇몇 태그를 이용해서 검증해보면 끔찍하다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:url="'url : ' + ${name}"></p>
<p th:email="'email : ' + ${name}"></p>
<p th:tel="'tel : ' + ${name}"></p>
<p th:number="'nubmer : ' + ${name}"></p>
</body>
</html>
다시 본론으로 넘어와서, 해당 클래스 내 doProcess() 메소드 내에서 각기 오버로딩 되어있는 getDisplayString() 메소드가 호출되는 것을 확인할 수 있다. 마지막 인자가 true 값으로 설정되어있으므로, 각 태그들에 대하여 html entity가 적용된다.
@Override
protected void doProcess(final ITemplateContext context,
final IProcessableElementTag tag,
final AttributeName attributeName, final String attributeValue,
final IThymeleafBindStatus bindStatus, final IElementTagStructureHandler structureHandler) {
String name = bindStatus.getExpression();
name = (name == null? "" : name);
final String id = computeId(context, tag, name, false);
// Thanks to precedence, this should have already been computed
final String type = tag.getAttributeValue(this.typeAttributeDefinition.getAttributeName());
// Apply the conversions (editor), depending on type (no conversion for "number" and "range"
// Also, no escaping needed as attribute values are always escaped by default
final String value =
applyConversion(type)?
SpringValueFormatter.getDisplayString(bindStatus.getValue(), bindStatus.getEditor(), true) :
SpringValueFormatter.getDisplayString(bindStatus.getActualValue(), true);
StandardProcessorUtils.setAttribute(structureHandler, this.idAttributeDefinition, ID_ATTR_NAME, id); // No need to escape: this comes from an existing 'id' or from a token
StandardProcessorUtils.setAttribute(structureHandler, this.nameAttributeDefinition, NAME_ATTR_NAME, name); // No need to escape: this is a java-valid token
StandardProcessorUtils.setAttribute(
structureHandler, this.valueAttributeDefinition, VALUE_ATTR_NAME, RequestDataValueProcessorUtils.processFormFieldValue(context, name, value, type));
}
SpringValueFormatter.getDisplayString(bindStatus.getValue(), bindStatus.getEditor(), true) :
SpringValueFormatter.getDisplayString(bindStatus.getActualValue(), true);
3. 결론
Thymeleaf 환경에서
text, hidden, datetime, datetime-local, date, month, time, week, number, range, email, url, search, tel, color 태그가 보인다면 딱 한번씩만 점검하고 넘어가자. 이
또한, 특정 상황에서는 text 태그가 입력값 검증 류의 취약점들의 대응방안으로 적용될 가능성도 있으니 기억해둬야겠다.