XSS 공격이란 무엇인지, Spring Boot에서 대응할 수 있는 방법은
해당 포스팅은 'xss(cross-site scripting) 공격의 개념과 해당 공격에 대해 spring boot 내부적으로 처리할 수 있는 대응 방법'을 정리한 내용입니다.
예전에 면접에서 스프링 부트에서 처리할 수 있는 보안 문제에 대해 아는 것이 있냐고 질문을 받았던 적이 있는데요. 당시에는 생각나는 것이 없어 대답을 하지 못했었는데 xss 공격도 처리해야 할 보안 문제 중 하나로 볼 수 있습니다.
XSS(Cross-site Scripting) 공격이란?
SQL Injection과 함께 웹 상에서의 취약점을 공격하는 기본적인 방법 중 하나인데요.
쉽게 공격자가 서버로 전달하는 데이터에 악의적인 스크립트 문을 삽입하여 개발자의 의도대로 동작하지 않게 하는 것을 말하며, 크게 'Reflected XSS'과 'Stored XSS' 두 가지로 분류할 수 있습니다.
* Cross-site Scripting의 약어가 CSS가 아닌 XSS인 이유는 기존에 CSS(Cascading Style Sheets)로 사용되고 있는 약어가 있기 때문입니다.
- Reflected XSS
공격자가 xss 공격에 취약한 웹 사이트(A 사이트라고 가정)를 미리 탐색하고 xss 공격을 위한 스크립트가 포함된 url을 사용자에게 노출시켜 클릭을 유도합니다.
사용자가 해당 url을 클릭할 경우 A 사이트의 서버에 스크립트가 포함된 url이 request로 전송되고, A 사이트의 서버에서는 해당 스크립트를 포함한 response를 사용자에게 전송(reflect, 반사)하게 됩니다.
개인적으로는 이론만 가지고 한 번에 이해하기가 어려웠는데, 쉽게 예를 들면 아래와 같습니다.
https://reflected-xss-example/search?keyword=helloWorld
//1. A 사이트에는 위와 같이 url 매개변수를 통해 사용자가 입력한 검색어를 수신하는 검색 기능이 있다고 가정합니다.
<p>Keyword: helloWorld</p>
//2. 그리고 해당 웹 사이트는 요청된 url에 대한 응답으로 입력된 검색어를 다시 반환합니다.
https://reflected-xss-example/search?keyword=<script>attackCode</script>
//3. 이때 서버에 xss 공격에 대한 처리가 되어있지 않은 경우 공격자는 아래와 같은 공격을 구성할 수 있으며, 해당 url을 사용자가 클릭하도록 유도합니다.
<p>Keyword: <script>attackCode</script></p>
//4. 사용자가 해당 url을 클릭했을 때 서버로부터 받는 결과는 위와 같으며, 공격자가 url의 매개변수를 통해 입력한 script 문이 실행되게 됩니다.
- Stored XSS
해당 방식은 Reflected XSS에 비해 익숙할 수 있는 방식으로 웹 사이트의 게시판에 스크립트 문을 삽입하는 공격입니다.
마찬가지로 공격자는 XSS 공격에 취약한 웹 사이트를 미리 탐색하고, 게시판을 통해 스크립트 문을 삽입합니다. 그리고 사용자가 해당 게시글을 확인할 때 script 문이 포함된 response가 반환되며 공격이 수행됩니다.
***
결론적으로 xss 공격은 사용자의 입력 값을 검증하지 않기 때문에 발생한다고 볼 수 있는데요.
공격자는 이러한 공격을 통해 사용자의 쿠키 또는 세션 정보를 획득하거나, 사용자를 악성 프로그램을 다운로드하는 사이트로 유도할 수 있게 됩니다.
Spring Boot에서 XSS 공격을 대응하는 방법
public class HTMLCharacterEscapes extends CharacterEscapes {
private final int[] asciiEscapes;
public HTMLCharacterEscapes() {
//xss 방지 처리할 특수 문자 지정
asciiEscapes = CharacterEscapes.standardAsciiEscapesForJSON();
asciiEscapes['<'] = CharacterEscapes.ESCAPE_CUSTOM;
asciiEscapes['>'] = CharacterEscapes.ESCAPE_CUSTOM;
asciiEscapes['('] = CharacterEscapes.ESCAPE_CUSTOM;
asciiEscapes[')'] = CharacterEscapes.ESCAPE_CUSTOM;
asciiEscapes['#'] = CharacterEscapes.ESCAPE_CUSTOM;
asciiEscapes['"'] = CharacterEscapes.ESCAPE_CUSTOM;
asciiEscapes['\''] = CharacterEscapes.ESCAPE_CUSTOM;
}
@Override
public int[] getEscapeCodesForAscii() {
return asciiEscapes;
}
@Override
public SerializableString getEscapeSequence(int ch) {
return new SerializedString(StringEscapeUtils.escapeHtml4(Character.toString((char) ch)));
}
}
(HTMLCharacterEscapes class)
대응 방법으로는 response를 클라이언트에게 내보내는 단계에서 처리하는 방식이 적용되었는데요.
먼저 태그 변환을 위해 CharacterEscapes 추상 클래스를 상속하는 HTMLCharcterEscapes 클래스를 생성합니다.
* getEscapeSequence() 메서드의 경우 아래 내용을 통해 이모지에서 발생할 수 있는 문제를 해결하는 코드가 추가되니 꼭 참고 부탁드립니다.
// https://mvnrepository.com/artifact/org.apache.commons/commons-text
implementation group: 'org.apache.commons', name: 'commons-text', version: '1.10.0'
(commons-text 의존성)
StringEscapseUtils를 사용하기 위해서는 다음과 같이 'commons-text' 의존성 추가가 필요합니다.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public MappingJackson2HttpMessageConverter jacksonEscapeConverter() {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build();
objectMapper.getFactory().setCharacterEscapes(new HTMLCharacterEscapes());
return new MappingJackson2HttpMessageConverter(objectMapper);
}
}
(WebMvcConfig class)
스프링 부트에서 @ResponseBody를 사용하게 되면 요청에 대한 응답을 반환하는 과정에서 ViewResolver 대신 HttpMessageConverter가 동작하게 되는데요. String과 같은 기본 문자에 대한 처리에는 StringHttpMessageConverter가 동작하게 되며, 객체의 경우에는 MappingJackson2HttpMessageConverter가 동작하게 됩니다.
따라서 여기서는 객체에 대해 동작하는 'MappingJackson2HttpMessageConverter'를 다음과 같이 직접 bean으로 등록하였습니다.
그리고 내부적으로 사용되는 Default ObjectMapper에 대해 setCharacterEscapes() 메서드를 통해 위에서 생성한 태그 변환 클래스를 추가해 줍니다.
이모지로 인해 발생하는 오류 처리 방법
Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Unmatched first part of surrogate pair (0xd83e)]
위와 같은 HTMLCharacterEscapes 클래스를 사용했을 때, request로 들어오는 이모지에 대해 위와 같은 Exception이 발생하는 것을 볼 수 있었는데요.
public class HTMLCharacterEscapes extends CharacterEscapes {
...
@Override
public SerializableString getEscapeSequence(int ch) {
char charAt = (char) ch;
if (Character.isHighSurrogate(charAt) || Character.isLowSurrogate(charAt)) {
StringBuilder sb = new StringBuilder();
sb.append("\\u");
sb.append(String.format("%04x", ch));
return new SerializedString(sb.toString());
} else {
return new SerializedString(StringEscapeUtils.escapeHtml4(Character.toString(charAt)));
}
}
}
(HTMLCharacterEscapes class)
이모지에 대해 발생하는 Exception은 getEscapeSequence() 메서드를 다음과 같이 적용함으로써 해결할 수 있는데요.
해당 코드로 문제가 해결되는 이유를 살펴보면 아래와 같습니다.
jvm은 내부적으로 문자열을 utf-16으로 다루며, 때문에 java도 utf-16을 기준(0x0000부터 0xFFFF)으로 문자열을 나타내도록 최초 개발되었습니다. 하지만 이후에 컴퓨터로 표현 가능한 문자가 utf-16의 기준의 20배가 넘는(0x10FFFF)만큼 늘어나면서 2byte로 표현을 할 수 없게 되었고, 이를 해결하기 위해 Surrogate Pair이라는 방법이 나왔는데요.
(Surrogate Pair는 2byte 두 개를 가지고 utf-32의 대안으로 문자를 표현하는 형식입니다.)
이모지에 의해 발생한 오류는 getEscapeSequences() 메서드 내부 동작 과정에서 변환 가능한 범위를 넘어선 Surrogate Pair에 대한 처리가 이루어지지 않기 때문에 발생한 문제로 해당 코드를 통해 Surrogate Pair에 대한 문제를 해결한 것입니다.
***
번외의 이야기지만, utf-16 인코딩의 경우 문자당 2byte를 사용하여 비효율적인 부분이 있기 때문에 java9부터는 내부적으로 Compact String이라는 기능을 도입하여 String이 ISO-8859-1 또는 Latin-1 문자만 포함하는 경우 1byte만 사용하도록 개선되었습니다.
< 참고 자료 >
https://inseok9068.github.io/springboot/springboot-xss-response/
https://jojoldu.tistory.com/470
< 이모지 오류에 대한 참고 자료 >
https://velog.io/@power0080/XSS-%ED%95%84%ED%84%B0%EC%99%80-%EC%9D%B4%EB%AA%A8%EC%A7%80
https://blog.naver.com/nakim02/221478419731
'Programming > Spring Boot' 카테고리의 다른 글
JPA save, saveAll 성능 차이가 발생하는 이유 (0) | 2023.08.26 |
---|---|
spring boot 로그 파일 남기는 방법 (logback-spring.xml 설정) (0) | 2023.08.20 |
(spring boot) Redis Key Event Notification 처리 방법 (0) | 2023.08.09 |
@AutoConfiguration을 사용하여 라이브러리 만들어보기 (0) | 2023.07.27 |
Spring Boot 자동 구성 AutoConfiguration 동작 원리 파헤치기 (0) | 2023.07.21 |