XSS(Cross Site Scripting)
17 min read
XSS 공격의 모든 것: 이해부터 방어까지
웹 보안을 이야기할 때 빠질 수 없는 주제가 바로 XSS(Cross Site Scripting) 공격이다. 오늘은 XSS가 무엇인지, 어떻게 작동하는지, 그리고 어떻게 방어할 수 있는지에 대해 자세히 알아보자.
능동적 공격 vs 수동적 공격
먼저 웹 공격의 기본적인 분류부터 살펴보자.
능동적 공격
능동적 공격은 공격자가 웹 애플리케이션에 직접 공격 코드를 보내는 공격 유형이다. SQL 인젝션이나 OS 명령 인젝션 같은 공격들이 여기에 해당한다.
수동적 공격
수동적 공격은 조금 다르다. 공격자가 미리 준비한 피싱 사이트를 이용해서 웹 애플리케이션에 방문한 사용자가 공격 코드를 실행하도록 만드는 공격이다. 페이지에 접근하거나 링크를 클릭하는 것만으로도 공격이 트리거된다. XSS, CSRF, 클릭재킹, 오픈 리다이렉트 등이 대표적인 예다.
흥미로운 점은 능동적 공격은 서버에서 대책을 세워야 하는 반면, 수동적 공격은 프론트엔드에서만 가능한 부분이 존재한다는 것이다.
XSS란 무엇인가?
XSS(Cross Site Scripting)는 웹 애플리케이션의 취약점을 이용해 악성 스크립트를 실행하는 공격이다.
일반적으로 교차 출처 페이지에 의한 자바스크립트는 SOP(Same Origin Policy)에 의해 실행되지 않는다. 하지만 XSS는 공격 대상 페이지에서 실행되기 때문에 이런 제약을 우회할 수 있다.
XSS 공격의 구조
XSS 공격의 기본 구조를 예시로 살펴보자. 공격자가 페이지 HTML에 악성 스크립트를 삽입해서 사용자가 그 스크립트를 실행하도록 만드는 것이 핵심이다.
예를 들어, 다음과 같은 URL이 쇼핑 사이트의 제품 검색 화면으로 이동하는 URL이라고 가정해보자:
https://site.example/search?keyword=보안
여기서 'keyword=보안'은 HTML에 다음과 같이 삽입된다:
<div id="keyword">검색 키워드: 보안</div>
<div id="result">...</div>
이때 공격자가 다음과 같은 키워드로 요청한다면 어떻게 될까?
https://site.example/search?keyword=<img src onerror="location.href=`https://attacker.example`" />
이미지 요소의 src가 제대로 설정되지 않아서 에러가 발생하고, 에러 처리를 위해 onerror 속성에 설정된 자바스크립트가 실행되면서 사용자가 강제로 다른 웹사이트로 이동하게 된다.
XSS의 위협
XSS 공격이 성공하면 다음과 같은 심각한 문제들이 발생할 수 있다:
- 보안 정보의 유출 - 쿠키, 세션 토큰 등 민감한 정보가 탈취될 수 있다
- 웹 애플리케이션 변조 - 페이지 내용이 악의적으로 변경될 수 있다
- 의도치 않은 조작 - 사용자 모르게 특정 행동을 수행하게 할 수 있다
- 사용자로 위장 - 공격자가 피해자인 척 행동할 수 있다
- 피싱 - 가짜 로그인 폼 등을 통해 계정 정보를 탈취할 수 있다
세 가지 XSS 유형
XSS 공격은 크게 세 가지 유형으로 분류된다.
1) 반사형 XSS (Reflected XSS)
반사형 XSS는 공격자가 준비한 함정에서 발생하는 요청에 잘못된 스크립트를 포함하는 HTML을 서버에서 생성해서 발생하는 XSS다. 요청 내용에 잘못된 스크립트가 포함된 경우에만 발생하며, 지속성이 없어서 비지속성 XSS라고도 한다.
2) 저장형 XSS (Stored XSS)
저장형 XSS는 공격자가 폼 등을 통해 제출한 악성 스크립트를 포함하는 데이터가 서버에 저장되고, 그것이 페이지에 반영되어 발생하는 XSS다. 데이터베이스에 등록된 데이터가 반영되는 페이지를 보는 모든 사용자에게 영향을 미친다. 반사형 XSS와는 달리 정상적인 요청을 하는 사용자에게도 영향을 줄 수 있어서 지속형 XSS라고도 한다.
3) DOM 기반 XSS (DOM-based XSS)
DOM 기반 XSS는 자바스크립트로 DOM을 조작할 때 발생하는 XSS다. 서버를 통하지 않기 때문에 감지가 어렵다는 특징이 있다.
DOM이란?
DOM은 HTML을 조작하기 위한 인터페이스다. 브라우저는 HTML 구문을 해석해서 DOM 트리를 생성한다. DOM 트리를 변경할 때는 자바스크립트를 사용한다:
document.body.innerHTML = `<a href="https://attacker.example">새로운 사이트로 이동</a>`;
DOM 기반 XSS 예시
https://site.example/#hello
같은 URL이 있다고 가정해보자. 여기서 #을 제거하고 hello라는 문자열을 DOM으로 삽입하는 코드가 다음과 같다:
const message = decodeURIComponent(location.hash.slice(1));
document.getElementById("message").innerHTML = message;
만약 https://site.example/#<img src=x onerror="location.href=
https://attacker.example`" />` 같은 URL을 요청한다면:
<div id="message">
<img src=x onerror="location.href=`https://attacker.example`" />
</div>
innerHTML
을 이용해서 DOM을 조작한 것이 XSS의 원인이 된다.
DOM 기반 XSS는 브라우저의 기능을 사용할 때 발생하며, 브라우저 기능은 소스(Source)와 싱크(Sink)로 분류된다:
- 소스: DOM 기반 XSS의 원인이 되는
location.hash
문자열 같은 것들location.href
,location.search
,location.hash
,document.referrer
,postMessage
,Web Storage
등
- 싱크: 소스의 문자열에서 자바스크립트를 생성하고 실행하는 것들
innerHTML
,eval
,location.href
,document.write
,jQuery()
등
XSS 방어 전략
실제 애플리케이션을 개발할 때는 XSS를 자동으로 예방해주는 라이브러리나 프레임워크를 사용하는 것이 좋다. 하지만 기본적인 방어 기법들도 알아두자.
1) 문자열 이스케이프 처리
이스케이프 처리란 프로그램에 특별한 의미를 갖는 문자나 기호를 특별하지 않은 의미로 변환하는 작업을 의미한다. <, > 기호를 인식해서 <script>
를 HTML 요소로 해석하는 것을 <script>
로 변환해서 막는 것이다:
const escapeHTML = (str) => {
return str.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
2) 속성값의 문자열을 쌍따옴표로 감싸기
HTML 속성값에 문자열을 넣으면 이스케이프 처리만으로는 예방할 수 없다. 쿼리 스트링 값에 onmouseover=alert(1)
같은 문자열이 저장되면 사용자가 마우스를 올렸을 때 alert(1)
이 실행된다.
<input type="text" value=onmouseover=alert(1) />
쌍따옴표로 묶으면 keyword 값이 단순한 문자열로 처리된다:
<input type="text" value="onmouseover=alert(1)" />
하지만 "onmouseover='alert(1)'
같이 적으면 value가 빈 배열로 들어가서 여전히 공격이 가능하다:
<input type="text" value="" onmouseover='alert(1)' />
쌍따옴표를 "
로 이스케이프 처리하면 문제를 해결할 수 있다:
<input type="text" value="" onmouseover='alert(1)'" />
3) 링크의 URL 스키마를 http/https로 제한하기
<a>
요소의 href 속성은 이스케이프와 쌍따옴표로 처리하는 데 한계가 있다:
const url = new URL(location.href).searchParams.get("url");
const a = document.querySelector("#my-link");
a.href = url;
위 코드의 경우 https://site.example/?url=javascript:alert(1)
같은 URL을 입력하면 공격이 가능하다:
<a id="my-link" href="javascript:alert(1)">링크</a>
a태그의 href 속성에 http 또는 https로만 제한해서 해결할 수 있다:
const url = new URL(location.href).searchParams.get("url");
if (url.match(/^https?:\/\//)) {
const a = document.querySelector('#my-link');
a.href = url;
}
4) DOM 조작을 위한 안전한 메서드와 프로퍼티 사용하기
DOM 기반 XSS는 innerHTML
등을 사용할 때 발생한다. 브라우저가 DOM을 조작할 때 HTML로 해석하는 API 사용을 피하면 DOM 기반 XSS를 예방할 수 있다.
5) 쿠키에 HttpOnly 속성 사용하기
로그인이 필요한 웹 서비스의 경우 세션 정보를 쿠키에 저장할 때가 많다. XSS 취약성이 존재하면 쿠키 값이 유출되어서 공격자가 사용자로 위장할 수 있다. HttpOnly 속성을 부여하면 자바스크립트로 쿠키 값을 가져올 수 없다.
6) 프레임워크 기능 활용하기
다양한 프로그래밍 언어나 프레임워크 중에는 XSS를 자동으로 예방해주는 것들이 있다. React, Vue.js, Angular 등이 대표적이다. 다만 React에서 dangerouslySetInnerHTML
을 사용하면 XSS 취약성이 발생할 수 있으니 주의해야 한다.
7) DOMPurify 라이브러리 사용하기
DOMPurify는 XSS로부터 무해한 HTML을 제외하고 일부 문자열만 제거하는 라이브러리다. sanitize
메서드를 사용해서 XSS로 작성된 문자열을 무효화할 수 있다:
const clean = DOMPurify.sanitize(dirty);
8) Sanitizer API 활용하기
Sanitizer API는 브라우저의 API로 DOMPurify와 비슷한 기능을 제공한다:
<script>
const el = document.querySelector("div");
const unsafeString = decodeURIComponent(location.hash.slice(1));
const sanitizer = new Sanitizer();
el.setHTML(unsafeString, sanitizer);
</script>
Content Security Policy (CSP)
CSP(Content Security Policy)는 XSS 같은 악성 코드를 포함한 인젝션 공격을 감지해서 피해를 막는 브라우저 기능이다.
CSP 개요
CSP는 Content-Security-Policy 헤더를 응답에 포함해서 활성화할 수 있다:
Content-Security-Policy: script-src *.trusted.example
CSP 헤더에 지정된 script-src *.trusted.com
같은 값을 policy directive 또는 directive라고 한다. directive에 지정되지 않은 호스트명의 서버에서는 자바스크립트 파일을 불러오지 않는다.
대표적인 directive들
directive | 의미 |
---|---|
script-src | 자바스크립트 등 스크립트 실행을 허용한다 |
style-src | CSS 등 스타일 적용을 허용한다 |
img-src | 이미지 불러오기를 허용한다 |
media-src | 사운드, 영상 불러오기를 허용한다 |
connect-src | XHR과 fetch 함수 등 네트워크 접근을 허용한다 |
default-src | 지정되지 않은 directive 전체를 허용한다 |
frame-ancestors | iframe 등으로 현재 페이지에 삽입하는 것을 허용한다 |
upgrade-insecure-requests | http://로 시작하는 URL 리소스를 https://로 시작하는 URL로 변환해서 요청한다 |
sandbox | 콘텐츠를 샌드박스화해서 외부로부터의 접근을 제어한다 |
소스 키워드
CSP에 지정할 수 있는 특별한 의미가 있는 소스 키워드들은 다음과 같다:
키워드 | 설명 |
---|---|
self | CSP로 보호하는 페이지와 동일 출처만 허용한다 |
none | 모든 출처를 허용하지 않는다 |
unsafe-inline | 인라인 스크립트 및 인라인 스타일을 허용한다 |
unsafe-eval | eval 함수를 허용한다 |
unsafe-hashes | DOM에 설정된 이벤트 실행을 허용한다. 그러나 인라인 스크립트, 자바스크립트 스키마 실행은 허용하지 않는다 |
Strict CSP
CSP를 적용한 페이지는 HTML 내에 자바스크립트를 작성하는 인라인 스크립트가 금지된다. unsafe-inline
을 사용하지 않고 안전하게 인라인 스타일 실행을 허용하기 위해서는 nonce-source, hash-source를 사용할 수 있다. 구글은 호스트명을 지정하는 대신 nonce-source, hash-source를 사용한 Strict CSP를 추천한다.
nonce-source
<script>
요소에 지정된 랜덤 토큰이 CSP 헤더에 지정된 토큰과 일치하지 않으면 에러가 발생한다.
hash-source
nonce-source와 비슷하지만 랜덤 토큰 대신 해시값을 사용한다. HTML을 동적으로 변경할 수 없는 경우 요청마다 토큰을 바꿀 수 없기 때문에 nonce-source 대신 hash-source를 사용한다.
strict-dynamic
nonce-source
, hash-source
를 사용하면 인라인 스크립트를 안전하게 실행할 수 있지만 동적인 <script>
요소 생성은 금지된다. strict-dynamic 키워드를 사용하면 <script>
요소를 동적으로 생성할 수 있다. 단, DOM 기반 XSS의 싱크인 innerHTML
과 document.write
기능은 제한된다.
object-src / base-uri
object-src는 플래시 같은 플러그인을 제한하는 directive다. base-uri는 <base>
요소를 제한하는 directive다.
마무리
XSS는 웹 보안에서 가장 기본적이면서도 중요한 공격 벡터 중 하나다. 다양한 유형의 XSS 공격을 이해하고, 적절한 방어 기법을 적용하는 것이 안전한 웹 애플리케이션을 만드는 첫걸음이다. 특히 최신 프레임워크들이 제공하는 보안 기능을 적극 활용하고, CSP 같은 브라우저 보안 기능도 함께 사용한다면 더욱 견고한 보안을 구축할 수 있을 것이다.
댓글 0개