다양한 경험을 하고 그 가치를 함께 나누는 것을 좋아합니다. 오픈소스컨설팅 프론트엔드 개발자 노은지입니다.
안녕하세요. App-Biz에서 Atlassian Add-on 개발을 하고있는 Frontend Developer 노은지입니다.
현대 웹 애플리케이션은 점점 더 복잡해지고 있으며, 보안 위협도 그에 따라 증가하고 있습니다. 사용자 데이터 보호 및 애플리케이션의 무결성을 유지하기 위해서는 프론트엔드 보안이 필수적입니다.
이 포스트에서는 제가 HTML을 DOM에 직접 삽입하는 dangerouslySetInnerHTML을 사용하면서 생길 수 있는 보안 문제인 XSS(교차 스크립트 공격)에 대하여 알아보고, 이에 맞서 프론트엔드 보안 강화를 위해 사용할 수 있는 라이브러리인 DOMPurify를 사용한 내용을 다루고 있습니다.
먼저 XSS (교차 스크립트 공격)이란 뭔지 한 번 짚고 넘어가 볼까요?
XSS(크로스 사이트 스크립팅, Cross-Site Scripting)은 웹 보안 취약점 중 하나로, 공격자가 악성 스크립트를 웹 페이지에 삽입하여 사용자의 브라우저에서 실행되도록 만드는 공격 기법입니다. 이 공격은 주로 웹 애플리케이션에서 사용자 입력을 제대로 검증하지 않거나 출력 시 적절한 처리를 하지 않을 때 발생합니다.
XSS는 크게 Stored XSS, Reflected XSS, DOM-based XSS 세 가지로 나뉩니다.
XSS 공격을 통해 공격자는 다음과 같은 행위를 할 수 있습니다:
저는 프로젝트에 어플리케이션의 다국어 처리를 위한 메시지에 줄바꿈을 처리하기 위해 <br/>
태그를 사용했습니다. 하지만 HTML을 직접 사용하면서 여러 가지 문제가 발생할 수 있다는 점을 깨달았습니다.
가장 큰 문제는 보안입니다. 사용자로부터 입력받은 데이터에 HTML 태그가 포함되어 있을 경우, 위에서 설명하였듯 DOM기반의 교차 스크립트 공격(XSS)의 위험이 커질 수 있습니다. 악의적인 사용자가 <script>
태그를 삽입하여 웹 페이지에서 임의의 스크립트를 실행할 수 있는 가능성이 존재합니다. 이러한 공격은 사용자 정보를 탈취하거나, 웹사이트의 기능을 방해하는 등 심각한 결과를 초래할 수 있습니다.
React에서는 DOM에 HTML을 직접 삽입하기 위하여 dangerouslySetInnerHTML
을 사용할 수 있지만, 이 경우 항상 HTML을 정제하여 보안을 강화해야 합니다. 저는 많은 방법 중 사용자 입력을 안전하게 처리할 수 있는 라이브러리인 DOMPurify를 활용하는 방법을 고려하였습니다.
dangerouslySetInnerHTML
메서드는 HTML 문자열을 그대로 DOM에 삽입하기 때문에, 만약 악성 스크립트가 포함된 HTML이 전달되면, 해당 스크립트가 실행될 수 있습니다.
예를 들어, 다음과 같은 코드가 있을 때:
<div dangerouslySetInnerHTML={{ __html: userInput }} />
이 코드에서 userInput
에 <script>alert('XSS!')</script>
와 같은 스크립트가 포함되어 있다면, 이 스크립트가 그대로 실행됩니다. 이로 인해 악성 스크립트가 실행되며, 이는 웹 페이지에서 XSS 공격을 유발할 수 있습니다.
즉, dangerouslySetInnerHTML
을 사용하면 사용자 입력이나 외부 데이터를 검증하지 않은 상태로 DOM에 삽입하게 되며, 이로 인해 공격자가 악성 스크립트를 실행할 수 있는 길을 열어줍니다.
다국어 처리를 위해 하드코딩 된 HTML을 사용하는 경우, 예를 들어 저와 같이 <br/>
태그를 직접 포함하는 경우에도 XSS 공격 위험이 있을 수 있습니다. 그 이유는 다음과 같습니다.
{username}
과 같은 동적 데이터를 포함하는 경우를 생각해볼 수 있습니다. 만약 이 데이터가 검증되지 않고 <script>
태그 같은 것이 포함되어 있다면, 결국 이 HTML은 실행되어 XSS 공격이 가능합니다.const message = `Welcome, ${username}! <br/> Enjoy your stay!`;
<div dangerouslySetInnerHTML={{ __html: message }} />
username
에 악성 스크립트가 들어오면 그대로 실행될 수 있습니다. 예를 들어, username
이 <script>alert('XSS!')</script>
라면 이 스크립트가 실행됩니다.DOMPurify는 웹 애플리케이션에서 사용자 입력을 안전하게 처리하기 위한 라이브러리로, 주로 XSS(교차 스크립트 공격)로부터 보호하기 위해 사용됩니다.
이 라이브러리는 HTML, SVG 및 MathML을 정제하여 악성 스크립트가 포함되지 않도록 도와줍니다. DOMPurify의 주요 기능과 특징은 다음과 같습니다:
DOMPurify를 사용하는 기본적인 예시는 다음과 같습니다
// DOMPurify, i18next 라이브러리 로드
import DOMPurify from 'dompurify';
import { t } from 'i18next';
// 다국어 처리를 위한 메시지
"I18N_MESSAGE_KEY01": "To manage your profile settings, go to the 'Profile' section in the main menu.<br/>Here, you can update your personal information, change your password, and configure your privacy settings.<br/>Make sure to save your changes before exiting."
/**
* 다국어 메시지를 안전하게 렌더링할 수 있는 함수
* @param {string} html - 안전하지 않은 HTML 메시지
* @returns {Object} - 안전한 HTML 메시지
*/
const sanitizeHtml = (html: string) => {
return { __html: DOMPurify.sanitize(html) };
};
// 정제된 HTML이 사용부에서 삽입 된 모습
return (
<ContentDescription dangerouslySetInnerHTML={sanitizeHtml(t('I18N_MESSAGE_KEY01'))} />
);
위의 예시에서 DOMPurify의 sanitizeHtml
메서드를 통해 악성 스크립트가 제거되어 최종적으로 정제된 HTML이 웹 페이지에 삽입됩니다.
사용 예시의 메시지 키를 만약 dangerouslySetInnerHTML를 사용하지 않고, 그대로 렌더링 하면 다음과 같이 보입니다.
반면 dangerouslySetInnerHTML를 사용하고, sanitizeHtml 함수를 통하여 악성 스크립트를 정제 한 뒤 화면에서의 모습은 다음과 같습니다.
dangerouslySetInnerHTML은 HTML을 직접 렌더링할 수 있는 강력한 도구이지만, 이를 사용할 때 사용자 입력을 포함한 데이터가 악성 스크립트에 의해 XSS 공격의 경로로 활용될 수 있습니다.
사용자의 입력을 받지 않는 하드코딩 된 메시지 같은 경우에도, 동적 데이터가 포함되거나 외부 번역 데이터가 삽입되는 경우 검증되지 않은 데이터가 있을 수 있으므로, 이를 처리할 때도 보안에 주의해야 합니다.
<프론트엔드 관련 포스트 더 보기>