stack-analysis
CHAPTER 106 / 120
읽기 약 2분
FUNCTION
XSS 방어: 출력 인코딩 + CSP + HttpOnly
핵심 개념
Reflected/Stored/DOM·React 자동·dangerouslySetInnerHTML — XSS 차단.
본문
XSS 3가지 종류
Reflected XSS:
- URL 파라미터에 스크립트
- 검색·에러 페이지에 입력값 반영
- 예: /search?q=<script>...</script>
Stored XSS:
- DB에 스크립트 저장
- 댓글·게시물 노출 시 실행
- 가장 위험
DOM-based XSS:
- 클라이언트 JS가 innerHTML 등에 입력값 직접 반영
- 서버 무관React — 자동 escape
// ✅ 안전 — React가 자동으로 텍스트로 처리
<div>{userInput}</div>
// → <script> 가 그대로 텍스트로 렌더
// ❌ 위험 — HTML로 해석
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// ✅ 안전 — 필요할 때 sanitize
import DOMPurify from 'isomorphic-dompurify';
<div dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href', 'title'],
})
}} />URL 파라미터 검증
// ❌ 위험
function ProfilePage() {
const params = useSearchParams();
const name = params.get('name');
return <h1>안녕하세요, {name}님</h1>;
// ?name=<script>...</script>
// → React 자동 escape로 안전
}
// ❌ 위험 — href에 직접
function Link() {
const params = useSearchParams();
const url = params.get('redirect');
return <a href={url!}>이동</a>;
// ?redirect=javascript:alert(1)
// → JavaScript URL 실행 가능
}
// ✅ 안전 — 프로토콜 검증
function Link() {
const params = useSearchParams();
const url = params.get('redirect');
if (url && !url.startsWith('/') && !url.startsWith('https://example.com/')) {
return null;
}
return <a href={url!}>이동</a>;
}마크다운 안전하게
// react-markdown — 자동 sanitize
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
// 외부 링크 안전
a: ({ href, children }) => {
if (href?.startsWith('http')) {
return <a href={href} rel="noopener noreferrer" target="_blank">{children}</a>;
}
return <a href={href}>{children}</a>;
},
}}
>
{userMarkdown}
</ReactMarkdown>
// 또는 marked + DOMPurify
import { marked } from 'marked';
import DOMPurify from 'isomorphic-dompurify';
const html = DOMPurify.sanitize(marked(userMarkdown));Cookie HttpOnly + Secure
// ❌ JS로 읽을 수 있는 쿠키
res.cookie('token', token);
// ✅ HttpOnly — JS 접근 불가
res.cookie('refreshToken', token, {
httpOnly: true,
secure: true, // HTTPS만
sameSite: 'strict', // CSRF 방어
maxAge: 7 * 86400 * 1000,
path: '/',
});
// → XSS 발생해도 토큰 도난 XCSP — XSS 마지막 방어선
script-src 'self' 'nonce-...';
// → 인라인 스크립트 차단 (nonce 없으면)
// → 외부 스크립트는 자기 도메인만
→ XSS 발생해도 스크립트 실행 차단이메일·전화번호 등 입력
// ✅ Zod로 형식 검증
const ProfileSchema = z.object({
name: z.string().min(1).max(50),
email: z.string().email(),
phone: z.string().regex(/^\d{3}-\d{4}-\d{4}$/),
bio: z.string().max(500),
website: z.string().url().optional(),
});
// 저장 전 한 번 더 trim
const data = ProfileSchema.parse(req.body);
const sanitized = {
...data,
name: data.name.trim(),
bio: data.bio.trim(),
};SVG 업로드 위험
// ❌ SVG 그대로 표시 — 스크립트 실행 가능
<img src={svgUrl} />
// ✅ DOMPurify로 sanitize
const svg = await fetch(svgUrl).then(r => r.text());
const clean = DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true } });
// 또는 SVG 업로드 자체 금지
const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/webp'];검증 도구
# 정적 분석
pnpm dlx eslint-plugin-security
# OWASP ZAP — 자동 스캔
docker run -t owasp/zap2docker-stable zap-baseline.py -t https://example.com
# Burp Suite — 수동 침투 테스트다음 챕터
CH.107 "CSRF 방어".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 코드의 XSS 방어 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
XSS 방어 관련 베스트 프랙티스 5가지를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 XSS 방어 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 시장의 XSS 방어 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
XSS 방어: 출력 인코딩 + CSP + HttpOnly는 이 3가지만 확실히 잡으세요
1.React 자동 escape + DOMPurify로 사용자 HTML — 99% XSS 차단
2.Cookie httpOnly로 토큰 보호 — XSS 발생해도 도난 X
3.CSP nonce + strict-dynamic = 인라인 스크립트 마지막 방어선
공유하기
진행도 106 / 120