사내에서 진행중인 프로젝트마다 취약성 검사를 진행하는데, 우리 팀에서 구축 중인 웹사이트가 XSS 공격에 취약하다는 진단을 받았다. XSS라는 단어는 개발을 하면서 어디선가 들어본 단어였지만, 정확히 무엇이고 어떻게 대처하는지는 그동안 잘 모르고 있었다. 따라서 XSS란 무엇이고 이를 어떻게 대처하면 좋을지 공부한 후에, 지금 진행중인 프로젝트에 적용하기로 했다.
XSS
란 Cross Site Script
의 약자로 웹사이트에 악성코드를 주입하는 행동을 말하며, 공격자가 웹사이트를 넘어서 공격한다는 뜻에서 유래되었다. 공격자는 웹사이트 입력 또는 출력 부분에 스크립트를 심어 웹사이트 뿐만 아니라 다른 사용자, 심지어 서버도 공격 가능하다는 특징이 있다. XSS는 대표적으로 Reflected XSS, Stored XSS, Dom based XSS 세 가지 유형으로 나뉜다.
가장 일반적인 XSS 유형으로 공격자가 입력한 스크립트가 즉시 실행되는 공격을 말한다. 공격자의 입력 값이 HTTP 응답에 그대로 포함되어 공격자에게 다시 반사(Reflected)되어 보인다해서 붙은 이름이다. 주로 주소창이나 간단한 입력 영역에서 스크립트를 삽입하는 식으로 공격을 진행한다.
vue를 이용해서 Reflected XSS를 재현해보자. 아래의 코드는 script라는 이름의 query값을 입력받아 화면에 출력하는 코드이다.
// Reflected XSS
<script setup lang="ts">
import { useRoute } from 'vue-router';
const route = useRoute();
const script = route?.query?.script ?? '';
</script>
<template>
<div>
<div id="target"> {{ script }}</div> // 문자열로 변환되어 출력
</div>
</template>
query에 단순한 문자열이 아니라 태그를 입력하거나 스크립트를 입력해도 화면에는 문자열로 변환되어 출력될 뿐이지만, 여기서 스크립트를 실행할 수 있는 방법이 있다.
주소창에 query로 script=alert('attack')
라는 문구를 입력한 후, 개발자 콘솔을 열어 eval(document.getElementById("target").innerHTML
를 입력하면 웹 사이트 상에서 뜨는 것을 볼 수 있다. 이처럼 XSS 공격은 즉각적으로 공격 결과가 나타나는 특징이 있다.
스크립트가 서버의 데이터베이스에 저장(Stored)되는 형태의 공격을 말한다. 데이터베이스에 저장이 된 스크립트는 일회성이 아니라 지속적으로 실행되기 때문에 XSS 공격 중에 가장 위험한 유형에 속한다.
게시판처럼 주로 사용자에게 입력받은 값을 서버에 저장해놓고, 저장해놓은 내용을 다시 사용자에게 출력하는 곳에서 발생한다. 공격자가 입력 부분에 스크립트를 삽입하여 서버에 업로드하고 이를 다른 사용자가 열람할 때, 공격자가 심어놓은 스크립트가 실행되어 사용자의 정보가 유출되거나 웹 사이트 자체를 공격할 수도 있다.
Stored XSS을 재현하는 코드는 아래와 같다. 해당 코드는 어느 게시판의 게시글을 열람할때 사용하는 로직이다. 게시판에 글을 쓰면 해당 글이 서버에 string 형태로 저장되고, 다른 사람이 해당 글을 읽을때 서버에 저장되어 있던 글을 client-side에 string 형태로 응답하여 전송한다고 가정한다.
// Stored XSS
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const tagString = ref('');
const loadData = () => {
tagString.value = `
<img src="/assets/computer-virus.png" />
<p>이제 뒤로가기 버튼을 누를때마다 팝업창이 뜹니다.</p>
`;
};
onMounted(()=>{
loadData();
});
</script>
<template>
<div>
<div class="inner" v-html="tagString"></div>
<button @click="loadData">그림 출력</button>
<router-link to=""><div @click="router.go(-1)">go back</div></router-link>
</div>
</template>
코드를 자세히 보면 string 안에 img 태그가 load됐을때 콜백함수가 지정되어 있는데, image의 load가 완료되면 window.addeventListener를 통해 뒤로가기를 할때마다 얼럿을 출력하는 로직이 설정되어 있다. 따라서 해당 게시글을 읽은 유저는 해당 사이트 내에 머무르는 한 뒤로가기 버튼을 누를때마다 얼럿이 계속해서 뜨게된다.
이처럼 Stored based XSS는 다른 HTML 문서를 보여주는 웹사이트나 게시판과 같이 공격자가 직접적으로 DOM을 작성할 수 있는 환경에서 주로 일어난다.
공격자가 공격 스크립트가 담긴 DOM을 작성 또는 수정한 후 다른 사용자가 해당 페이지를 열어보게 하여 실행되는 공격이다. page 파일 원본의 소스는 변경되지 않지만, 현재 client-side에 생성된 DOM을 직접 조작하여 공격을 일으킨다. Dom based XSS는 엄밀히 따지면 Reflected XSS 형태로 또는 Stored XSS 어느 형태로든지 나타날 수 있다.
query로 직접적인 DOM의 제어를 하지않고, 공격자로부터 직접 입력받는 곳과 입력하는 출력하는 곳의 코드를 sanitize
해야한다. 코드를 sanitize하는 방법은 정규식으로 걸러내거나 별도의 로직을 구현하는 등 많은 방법이 있지만, 현재 가장 많이 사용하고 검증된 sanitize 라이브러리를 사용하는 것이 제일 안전하다.
Sanitize란?
Sanitize는 영어로 소독이라는 뜻인데, 의미 그대로 코드를 소독한다는 뜻이다. 사용자가 tag나 script 자체를 입력하지 못하게 하거나, 입력하더라도 입출력 시 코드를 sanitize하여 공격에 사용될만한 내용들을 걸러내야 한다.
본 포스트에서는 sanitize-html 라이브러리로 code를 sanitize를 하는 방법을 설명한다.
// Reflected XSS
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import sanitizeHtml from 'sanitize-html';
const route = useRoute();
const router = useRouter();
// query를 sanitize
const script = sanitizeHtml(route?.query?.script) ?? '';
</script>
<template>
<div>
<div id="target">Your script is : {{ script }}</div>
<router-link to=""><div @click="router.go(-1)">go back</div></router-link>
</div>
</template>
Reflected XSS 코드 예제에서는 query를 받는 부분을 sanitizeHtml() 함수를 사용하여 sanitize를 한다. 코드를 적용하니 주소 url에서 query에 태그를 입력해도 태그가 걸러지는 것을 알 수 있다.
Stored XSS 코드 예제에서 tagString 문자열을 sanitizeHtml() 함수로 감싼다. 다만 여기서는 태그는 허용해야하고, 스크립트만 막아야하므로 별도의 옵션으로 allowedTags를 false값으로 주었다.
// Stored XSS
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import sanitizeHtml from 'sanitize-html';
const router = useRouter();
let tagString = ref('');
const loadData = () => {
tagString.value = sanitizeHtml(
`
<img src="/assets/computer-virus.png" />
<p>이제 뒤로가기 버튼을 누를때마다 팝업창이 뜹니다.</p>
`,
{
allowedTags: false, // 모든 태그 허용
},
);
};
</script>
<template>
<div>
<div class="inner" v-html="tagString"></div>
<button @click="loadData">그림 출력</button>
<router-link to=""><div @click="router.go(-1)">go back</div></router-link>
</div>
</template>
왼쪽이 sanitize 전, 오른쪽이 sanitize 후 모습이다. img 태그는 유지된 채로 onload attribute는 제거된 모습을 볼 수 있다.
XSS(Cross Site Scripting) 공격이란?
Reflected XSS(반사된 XSS)
DOM 기반 XSS(DOM based Cross Site Scripting) 공격과 방어