본문 바로가기
개발 · IT/보안 · 시큐어 코딩

XSS 방어 코드 — 쉽게 적용하는 방법

by 플라퉁 2025. 12. 4.
반응형

XSS 방어 코드 — 쉽게 적용하는 방법

보안 이미지

웹 개발자가 당장 적용할 수 있는 실용적 XSS(교차 사이트 스크립팅) 방어 방법을 정리했습니다. 예제 코드는 프론트엔드와 백엔드를 모두 다룹니다.

핵심 요약
  • 출력 시점에 인코딩/이스케이프(escape) 하라 — 가장 기본이자 강력한 방어
  • 절대로 innerHTML로 사용자 입력을 그대로 넣지 마라
  • 콘텐츠는 가능한 경우 텍스트로 삽입하고, 허용된 HTML만 정해진 sanitizer로 정리하라
  • Content-Security-Policy(CSP)로 실행 가능한 리소스를 제한하라
  • 쿠키는 HttpOnly, Secure, SameSite 설정을 사용하라

1. XSS가 뭔가요? (간단히)

사용자 입력(댓글, 폼 데이터, URL 파라미터 등)이 웹 페이지에 적절히 처리되지 않고 그대로 브라우저에서 실행될 때 발생하는 공격입니다. 공격자는 스크립트를 주입해 쿠키 탈취, 세션 하이재킹, 악성 행위 수행 등이 가능합니다.

2. 바로 적용 가능한 실전 방어법

2.1 출력 시점에 이스케이프(escape) — 기본 중의 기본

HTML 문서에 사용자 데이터를 넣을 때는 반드시 HTML 엔티티로 변환(예: <, >, &, ")하세요.

브라우저(순수 JS) — 안전한 출력 함수
function escapeHtml(str) {
  if (typeof str !== 'string') return str;
  return str
    .replace(/&/g, '&')
    .replace(/</g, '<')   // 이미 이스케이프된 경우를 일부러 방지하려면 추가 로직 필요
    .replace(/</g, '<')
    .replace(/>/g, '>')
    .replace(/"/g, '"')
    .replace(/'/g, ''');
}

// 사용 예
const userInput = '<script>alert(1)</script>';
const el = document.getElementById('comment');
el.textContent = escapeHtml(userInput); // textContent로도 안전하게 넣을 수 있음

2.2 DOM 조작 시 textContent / setAttribute 사용

DOM에 값을 넣을 때는 innerHTML 대신 textContent를 사용하세요. URL 같은 속성은 setAttribute로 설정합니다.

2.3 신뢰된 프레임워크 템플릿 기능 활용

React, Vue, Thymeleaf, JSP 등 템플릿 엔진/프레임워크는 기본적으로 출력 이스케이프 기능을 제공합니다. 가능한 한 템플릿의 escape 기능을 사용하세요.

2.4 허용된 HTML만 허용할 때 — sanitizer 사용

리치 텍스트(사용자가 HTML 입력 허용)를 다뤄야 한다면, 신뢰할 수 있는 sanitizer 라이브러리(예: DOMPurify 등)를 사용해 허용 태그/속성만 남기세요.

DOMPurify 사용 예 (간단)
// 프론트엔드에 DOMPurify가 포함되어 있다고 가정
const dirty = '<img src=x onerror=alert(1)><b>hello</b>';
const clean = DOMPurify.sanitize(dirty, {ALLOWED_TAGS: ['b','i','u','a','p','br'], ALLOWED_ATTR: ['href']});
container.innerHTML = clean; // sanitize 후에만 innerHTML 사용

2.5 서버에서의 안전 조치

서버는 항상 신뢰할 수 없는 입력을 의심해야 합니다. 가능한 서버 쪽에서도 출력 시 이스케이프를 적용하거나, 저장 전에 정제(sanitize)하세요. 또한 XSS 외 다른 공격(예: CSRF)도 함께 방어해야 합니다.

Express 예제 — helmet + CSP 설정
// server.js (Node/Express)
const express = require('express');
const helmet = require('helmet');
const app = express();

// 기본적 보안 헤더
app.use(helmet());

// CSP 예시 (최소 권장)
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"], // 필요하면 해시나 nonce 추가
    objectSrc: ["'none'"],
    upgradeInsecureRequests: [],
  }
}));

// 쿠키 설정 예시
// res.cookie('sid', token, { httpOnly: true, secure: true, sameSite: 'Strict' });

2.6 CSP(콘텐츠 보안 정책) 적극 활용

CSP는 외부 스크립트/스타일 실행을 제어합니다. script-src 'self' 'nonce-...' 같은 정책을 사용하면 인라인 스크립트 실행을 막고, 허가된 스크립트만 허용할 수 있습니다.

2.7 쿠키 보안: HttpOnly, Secure, SameSite

쿠키 탈취를 막기 위해 세션 쿠키는 반드시 HttpOnlySecure를 사용하고, 가능하면 SameSite=Strict 혹은 Lax를 설정하세요.

3. 현장에서 자주 쓰는 '간단 체크리스트'

  • 사용자 데이터는 출력 직전에 이스케이프(또는 sanitizer) 한다.
  • 템플릿 엔진의 자동 이스케이프를 끄지 않는다.
  • DOM 삽입 시 textContent / setAttribute를 우선 사용한다.
  • 리치 텍스트는 DOMPurify 같은 라이브러리로 정제한다.
  • 모든 중요한 응답에 CSP 헤더를 설정한다.
  • 세션 쿠키는 HttpOnly/ Secure / SameSite로 설정한다.
  • 외부 스크립트(3rd-party)는 최소화하고, 신뢰 가능한 소스만 허용한다.

4. 짧은 FAQ

Q: "innerHTML은 절대 못 쓰나요?"
A: innerHTML 자체가 금지된 것은 아닙니다. 단, 사용자 입력을 넣을 때는 반드시 신뢰 가능한 sanitizer(또는 이스케이프)를 거친 뒤에만 사용하세요.

Q: "React를 쓰면 XSS 걱정이 없나요?"
A: 대부분 안전하지만, dangerouslySetInnerHTML 같은 기능을 쓸 때는 입력 정제가 필수입니다.

5. 마무리: 간단한 적용 우선순위 (초급→중급)

  1. 템플릿/렌더러의 자동 이스케이프 확인
  2. DOM 삽입 시 textContent 사용
  3. 입력 필드/폼에 대한 최소한의 검증 (형식 검사)
  4. 리치 텍스트 필요 시 sanitizer 도입
  5. CSP, 쿠키 보안, helmet 같은 헤더 적용
실행 팁

작게 시작하세요. 우선 템플릿의 자동 이스케이프와 textContent 사용을 적용한 뒤, 리치 텍스트 처리와 CSP를 순차적으로 적용하면 큰 문제 없이 보안 수준을 올릴 수 있습니다.

참고: 실제 서비스에서는 XSS 외에도 CSRF, 클릭재킹, 인젝션 공격 등을 함께 고려해야 합니다. 코드 적용 전 단위 테스트와 간단한 보안 스캔(정적분석/동적분석)을 권장합니다.

반응형

댓글