보안테스트 결과 XSS 취약점이 발견돼 조치를 하면서, 약간 고생을 해서 경험을 공유해보려 한다.
XSS(Cross Site Scripting)
악의적인 사용자가 공격하려는 사이트에 스크립트를 넣는 기법
XSS Filter
현재 백엔드 프로젝트가 Rest API로 구성돼, 대부분의 요청과 응답이 json 방식으로 요청 중이다. Spring XSS를 검색하면 가장 많이 노출되는 Lucy Filter가 있지만, @ResponseBody로 응답되는 application/json 타입은 처리하지 못한다고 해서 Jackson 라이브러리의 CharacterEscape를 설정해 <, >, ', " 총 4개의 문자를 치환했었다.
관련된 설정은 https://jojoldu.tistory.com/470을 참고했다.
위의 설정으로 DB에 저장될 때는 그대로 저장되지만, 클라이언트에게 응답할 때 Jackson 라이브러리의 MappingJackson2HttpMessageConverter가 따옴표와 꺾쇠를 치환하면서 대부분의 XSS 공격을 무효화할 수 있었다.
취약점
다만 사용자들이 글을 작성할 수 있는 게시판 같은 곳에서 웹에디터(ckeditor5)를 사용하고 있는데, HTML 기반으로 작동하다 보니 화면에서 보여줄 때 HTML escape 된 내용을 다시 풀어줘야지 작성된 내용이 정상적으로 보인다(XSS Filter에서 치환된 문자를, 다시 원복 해야 함).
그러다 보니 웹에디터를 사용한 곳에서 XSS 공격에 노출되는 문제점이 있었다.
XSS Util
웹에디터 자체적인 기능도 있고, 다른 자바스크립트 오픈소스 라이브러리를 사용을 추천하는 글도 있었다. 하지만 현재 프로젝트에서는 프론트 서버가 따로 없다 보니 클라이언트에서 모두 조작이 가능해 위험했다.
결국은 백엔드 자바 서버에서 필터링해야 하는 것이라, 웹에디터 내용을 응답하는 로직에서 사용할 XSS 유틸을 만들어서 사용했다.
@ParameterizedTest
@ValueSource(strings = {
"<p>>XSS><b class='red' onclick=alert(1)>attack</b></p>",
"<p class='red' onpointerdown= 'alert(1)'></p>",
"<p></p><iframe class='red' src='www.naver.com' width=500 height=500;"
})
@DisplayName("xss 위험한 단어는 제거한다.")
void prevent_xss(String attack) {
String cleaned = this.cleanXss(attack);
System.out.println(cleaned);
Assertions.assertThat(cleaned)
.contains("<p", "</p>", "class='red'")
.doesNotContain("onclick", "alert", "iframe");
}
private String cleanXss(String text) {
Pattern onEventPattern = Pattern.compile("(<\\s?[^>]+\\s)" +
"(on[\\w\\s]+?=\\s*'[^']*'" +
"|on[\\w\\s]+?=\\s*\"[^\"]*\"" +
"|on[\\w\\s]+?=[^>]*)" +
"([^>]*>)");
Pattern blacklist = Pattern.compile("script|iframe|frame(set)|eval|javascript", Pattern.CASE_INSENSITIVE); // 위험한 태그들, etc...
String unescaped = StringEscapeUtils.unescapeHtml(text);
String eventRemoved = onEventPattern.matcher(unescaped)
.replaceAll("$1$3");
return blacklist.matcher(eventRemoved)
.replaceAll("");
}
cleanXss라는 함수를 유틸로 제공했었는데, 간단히 내용을 설명하면 아래와 같다.
- 내용을 unescape(<을 <로 치환)
- 태그 안의 on... 이벤트를 제거
- 위험한 태그명을 제거
하지만 앞서 말했듯이, 보안테스트 결과 XSS 공격에 노출됐다. 범인은 공백이었다.
취약점
정규식에서 는 공백이 아니다.
기존의 on 이벤트를 제거하는 정규식에서 공백을 의미하는 \s 표현식을 사용했었다. 당연히 모든 공백을 매칭할 줄 알았는데 &npsp;는 HTML 파싱 될 때만 공백으로 인식하는 특정 문자열로 정규식에서는 일반문자로 취급된다는 것이다.
결국 아래와 같은 XSS 공격에 무방비하게 노출 돼버려 취약점에 걸린 것이다.
조치
\p{Z} or \p{Separator}: any kind of whitespace or invisible separator.
https://www.regular-expressions.info/unicode.html
다행히도 기존 공백 정규식과 함께 도 같이 인식할 수 있는 표현식이 있다. \s로 작성된 부분을 \p{Z}로 변경하면 도 공백패턴으로 인식할 수 있다.
Pattern onEventPattern = Pattern.compile("(<\\p{Z}?[^>]+\\p{Z})" +
"(on[\\w\\p{Z}]+?=\\p{Z}*'[^']*'" +
"|on[\\w\\p{Z}]+?=\\p{Z}*\"[^\"]*\"" +
"|on[\\w\\p{Z}]+?=[^>]*)" +
"([^>]*>)");
사실 XSS의 경우 잘 만들어진 오픈소스 라이브러리가 많기 때문에 직접 정규식을 작성하는 것은 상대적으로 위험하다. 나같은 경우에는 요구사항이 on으로 시작하는 이벤트만 제거해달라는 요구사항이 있어서 불가피하게 정규식을 작성했지만, 대부분의 상황에서는 라이브러리를 사용하는게 생산성과 안전성, 그리고 건강에 좋을듯하다.
XSS 공격에 성공한 문구를 전달 받았을 때 가 스페이스바(공백)과 똑같이 보이는 상태였는데, 이를 복붙하다보니 원인을 파악하는데만 2시간 정도 걸렸다.
'Backend > Java' 카테고리의 다른 글
Spring Actuator로 Property 변경하기 (0) | 2023.08.29 |
---|---|
오라클 - 마이바티스 날짜형 데이터 맵핑 (0) | 2023.07.12 |
Java에서 Null을 다루는 방식 (0) | 2023.05.22 |
공통 lib -> spring-boot-starter로 바꾸기(3) (0) | 2023.04.23 |
Stream에 Decorator 패턴 써먹기 (2) | 2023.04.03 |