안녕하세요. App-Biz에서 Atlassian Add-on 개발을 하고있는 Frontend Developer 노은지입니다.
현대 웹 애플리케이션은 점점 더 복잡해지고 있으며, 보안 위협도 그에 따라 증가하고 있습니다. 사용자 데이터 보호 및 애플리케이션의 무결성을 유지하기 위해서는 프론트엔드 보안이 필수적입니다.
이 포스트에서는 제가 HTML을 DOM에 직접 삽입하는 dangerouslySetInnerHTML을 사용하면서 생길 수 있는 보안 문제인 XSS(교차 스크립트 공격)에 대하여 알아보고, 이에 맞서 프론트엔드 보안 강화를 위해 사용할 수 있는 라이브러리인 DOMPurify를 사용한 내용을 다루고 있습니다.
XSS (교차 스크립트 공격)
먼저 XSS (교차 스크립트 공격)이란 뭔지 한 번 짚고 넘어가 볼까요?
XSS(크로스 사이트 스크립팅, Cross-Site Scripting)은 웹 보안 취약점 중 하나로, 공격자가 악성 스크립트를 웹 페이지에 삽입하여 사용자의 브라우저에서 실행되도록 만드는 공격 기법입니다. 이 공격은 주로 웹 애플리케이션에서 사용자 입력을 제대로 검증하지 않거나 출력 시 적절한 처리를 하지 않을 때 발생합니다.
XSS의 유형
XSS는 크게 Stored XSS, Reflected XSS, DOM-based XSS 세 가지로 나뉩니다.
- Stored XSS (저장형 XSS):
- 공격자가 악성 스크립트를 서버에 저장하고, 해당 스크립트가 다른 사용자의 브라우저에서 실행되도록 하는 방식입니다.
- 예를 들어, 공격자가 웹사이트의 댓글, 게시글 등의 입력 필드에 악성 스크립트를 삽입하면, 다른 사용자가 해당 페이지를 방문할 때 이 스크립트가 실행됩니다.
- 피해 범위가 클 수 있습니다.
- Reflected XSS (반사형 XSS):
- 서버가 공격자의 악성 스크립트를 사용자에게 즉시 반환하여 사용자의 브라우저에서 실행되도록 하는 방식입니다.
- 주로 URL에 악성 코드를 포함하여 전송한 후, 사용자가 그 URL을 클릭하면 브라우저에서 스크립트가 실행됩니다.
- 주로 이메일 피싱 링크나 악성 URL을 통해 이루어집니다.
- DOM-based XSS (DOM 기반 XSS):
- 서버가 아닌 클라이언트 측에서, 즉 브라우저에서 직접적으로 DOM을 조작하여 악성 스크립트가 실행되는 방식입니다.
- 주로 자바스크립트 코드가 DOM을 동적으로 처리할 때 발생하며, 서버에 영향을 미치지 않고 클라이언트 쪽에서만 발생하는 것이 특징입니다.
XSS 공격의 위험성
XSS 공격을 통해 공격자는 다음과 같은 행위를 할 수 있습니다:
- 쿠키 탈취: 공격자가 사용자의 세션 쿠키를 탈취하여 세션 가로채기(Session Hijacking)를 시도할 수 있습니다.
- 사용자 인증 정보 도용: 사용자의 로그인 정보나 개인 정보를 훔칠 수 있습니다.
- 피싱: 악성 스크립트를 통해 사용자가 악의적인 사이트에 정보를 입력하도록 유도할 수 있습니다.
- 키로깅: 사용자의 키 입력을 기록하여 민감한 정보를 수집할 수 있습니다.
- 웹사이트 변조: 웹 페이지의 내용이 악성 스크립트에 의해 변경되어 사용자가 혼란스러워지거나 신뢰성을 잃을 수 있습니다.
dangerouslySetInnerHTML
저는 프로젝트에 어플리케이션의 다국어 처리를 위한 메시지에 줄바꿈을 처리하기 위해 <br/>
태그를 사용했습니다. 하지만 HTML을 직접 사용하면서 여러 가지 문제가 발생할 수 있다는 점을 깨달았습니다.
가장 큰 문제는 보안입니다. 사용자로부터 입력받은 데이터에 HTML 태그가 포함되어 있을 경우, 위에서 설명하였듯 DOM기반의 교차 스크립트 공격(XSS)의 위험이 커질 수 있습니다. 악의적인 사용자가 <script>
태그를 삽입하여 웹 페이지에서 임의의 스크립트를 실행할 수 있는 가능성이 존재합니다. 이러한 공격은 사용자 정보를 탈취하거나, 웹사이트의 기능을 방해하는 등 심각한 결과를 초래할 수 있습니다.
React에서는 DOM에 HTML을 직접 삽입하기 위하여 dangerouslySetInnerHTML
을 사용할 수 있지만, 이 경우 항상 HTML을 정제하여 보안을 강화해야 합니다. 저는 많은 방법 중 사용자 입력을 안전하게 처리할 수 있는 라이브러리인 DOMPurify를 활용하는 방법을 고려하였습니다.
dangerouslySetInnerHTML가 위험한 이유?
dangerouslySetInnerHTML
메서드는 HTML 문자열을 그대로 DOM에 삽입하기 때문에, 만약 악성 스크립트가 포함된 HTML이 전달되면, 해당 스크립트가 실행될 수 있습니다.
예를 들어, 다음과 같은 코드가 있을 때:
<div dangerouslySetInnerHTML={{ __html: userInput }} />
이 코드에서 userInput
에 <script>alert('XSS!')</script>
와 같은 스크립트가 포함되어 있다면, 이 스크립트가 그대로 실행됩니다. 이로 인해 악성 스크립트가 실행되며, 이는 웹 페이지에서 XSS 공격을 유발할 수 있습니다.
즉, dangerouslySetInnerHTML
을 사용하면 사용자 입력이나 외부 데이터를 검증하지 않은 상태로 DOM에 삽입하게 되며, 이로 인해 공격자가 악성 스크립트를 실행할 수 있는 길을 열어줍니다.
하드코딩한 다국어 메시지에도 XSS 공격이 가능할까?
다국어 처리를 위해 하드코딩 된 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>
라면 이 스크립트가 실행됩니다.
- 외부 라이브러리나 API로부터 데이터 주입:
- 다국어 처리를 위해 사용하는 번역 파일 또는 외부 API에서 번역 데이터를 가져오는 경우, 해당 데이터가 안전하지 않으면 하드코딩된 메시지라도 공격에 노출될 수 있습니다.
- 예를 들어, 번역 파일에 문제가 있거나 API에서 불안전한 데이터가 전달될 경우, 이 데이터가 DOM에 삽입되면서 XSS 공격의 원인이 될 수 있습니다.
- 실수로 삽입된 악성 데이터:
- 개발자가 하드코딩하는 과정에서 실수로 악성 데이터를 포함하거나, 협업 중 누군가가 악성 스크립트를 코드에 추가할 수 있습니다. 보통 하드코딩된 HTML이 안전하다고 생각하지만, 실수나 관리 부주의로 인해 잠재적인 XSS 취약점이 생길 수 있습니다.
3. XSS 공격 방지 방법
- 사용자 입력 검증:
- 사용자로부터 입력받은 데이터는 반드시 검증하고, 악성 코드가 포함되어 있지 않도록 필터링해야 합니다. HTML 태그나 자바스크립트 코드를 문자열 그대로 삽입하는 것을 피하고, 적절하게 인코딩 또는 이스케이프 처리를 해야 합니다.
- DOMPurify 등의 라이브러리 사용:
- HTML을 렌더링할 때, DOMPurify와 같은 검증된 라이브러리를 사용하여 악성 스크립트를 정화할 수 있습니다. 이 라이브러리는 HTML을 안전하게 처리하고, 스크립트나 이벤트 핸들러를 제거하여 XSS 공격을 방지합니다.
DOMPurify
DOMPurify는 웹 애플리케이션에서 사용자 입력을 안전하게 처리하기 위한 라이브러리로, 주로 XSS(교차 스크립트 공격)로부터 보호하기 위해 사용됩니다.
이 라이브러리는 HTML, SVG 및 MathML을 정제하여 악성 스크립트가 포함되지 않도록 도와줍니다. DOMPurify의 주요 기능과 특징은 다음과 같습니다:
주요 기능
- XSS 방어: DOMPurify는 사용자가 입력한 HTML 코드에서 악성 스크립트, 이벤트 핸들러, 또는 기타 위험한 요소를 제거합니다. 이를 통해 웹 페이지에서 스크립트가 실행되는 것을 방지할 수 있습니다.
- 브라우저 호환성: DOMPurify는 대부분의 현대 브라우저에서 잘 작동하며, 다양한 플랫폼과 환경에서 사용할 수 있습니다.
- 간편한 사용법: 라이브러리를 사용하기 위해 복잡한 설정이 필요하지 않으며, 간단한 API로 손쉽게 통합할 수 있습니다. HTML 문자열을 정제하기 위한 단 한 줄의 코드로 사용할 수 있습니다.
- 신뢰성 있는 정제: DOMPurify는 검증된 보안 알고리즘을 사용하여 정제 과정을 수행하며, 보안 커뮤니티에서 널리 사용되고 있습니다. GitHub에서도 활발하게 유지 관리되고 있습니다.
- 옵션 설정: DOMPurify는 다양한 설정 옵션을 제공하여 사용자가 필요에 따라 정제 프로세스를 세부 조정할 수 있습니다. 예를 들어, 특정 HTML 태그나 속성을 허용하거나 차단할 수 있습니다.
사용 예시
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 공격의 경로로 활용될 수 있습니다.
사용자의 입력을 받지 않는 하드코딩 된 메시지 같은 경우에도, 동적 데이터가 포함되거나 외부 번역 데이터가 삽입되는 경우 검증되지 않은 데이터가 있을 수 있으므로, 이를 처리할 때도 보안에 주의해야 합니다.
<프론트엔드 관련 포스트 더 보기>