jQuery DOM 기반 크로스 사이트 스크립팅 (DOM XSS)

DOM-based Cross-Site Scripting (DOM XSS)

설명

DOM 기반 XSS는 DOM에서 읽은 텍스트나 속성 값(예: textContent/innerText/value/attr)이 다시 HTML로 해석되어 삽입될 때 발생합니다. jQuery에 사용자 제어 문자열을 $() 인자로 넘기면 HTML로 파싱되거나, .html()/.append()에 문자열을 주면 브라우저가 태그와 이벤트 핸들러를 실행해 스크립트가 동작할 수 있습니다. 이미 이스케이프된 텍스트라도 재해석되면 이스케이프가 무력화되어 공격 코드가 실행될 수 있습니다.

잠재적 영향

  • 세션 탈취 및 사용자 가장: 공격자는 스크립트가 읽을 수 있는 쿠키·토큰·브라우저 저장소 값을 탈취하거나 사용자를 가장할 수 있습니다.
  • 민감 정보 유출: 화면/폼의 데이터, 입력값, CSRF 토큰 등을 읽어 외부로 전송할 수 있습니다.
  • DOM 변조 및 피싱: 페이지 내용을 바꿔 가짜 입력 폼/팝업을 띄워 자격증명을 수집할 수 있습니다.
  • 임의 요청 전송: 사용자의 권한으로 백엔드 API 호출, 계정 설정 변경 등을 유발할 수 있습니다.
  • 지속적 오염: 속성/Storage/URL에 저장된 값이 반복적으로 재해석되면 재방문 시에도 악성 스크립트가 실행될 수 있습니다.

해결 방법

  • DOM에서 읽은 텍스트/속성 값을 HTML로 직접 해석하지 말고 textContent/innerText 또는 jQuery의 .text()로 출력하세요.
  • 사용자 제어 문자열을 셀렉터로 사용할 때는 jQuery($str) 대신 $.find(str)을 사용해 HTML 파싱을 피하세요. 가능하면 허용 목록(allowlist)으로 셀렉터를 제한하세요.
  • 불가피하게 HTML을 삽입해야 한다면 DOMPurify 같은 검증된 Sanitizer로 정화한 뒤 사용하세요. 허용 태그/속성은 최소화하세요.
  • jQuery .html()/.append()/.before()/.after()에 문자열을 직접 넘기지 말고, 필요한 경우 document.createTextNode 또는 안전한 DOM 노드를 사용하세요.
  • URL의 query/hash, data-* 속성, input.value 등 사용자 제어 값은 항상 검증/정화 후 사용하세요.
  • 방어 심층(Defense-in-Depth)으로 CSP(Content Security Policy)를 적용해 인라인 스크립트 실행을 차단하세요.

예시

변경 후의 A·B·C는 텍스트 출력, 제한된 HTML 표시, 허용된 요소 선택이라는 서로 다른 목적을 보여줍니다. 셀렉터를 HTML로 파싱하지 않는 것만으로 선택 범위까지 제한되지는 않으므로 허용 목록도 유지하세요.

변경 전

javascript
// DOM에서 읽은 문자열을 HTML로 재해석하여 삽입 (취약)
$(function () {
  // 사용자 제어 가능: 서버 템플릿/Storage/URL에서 복원된 data-content
  const raw = $("#card").attr("data-content");
  // 1) 문자열을 그대로 .html()로 삽입 -> 이벤트 핸들러/스크립트 실행 위험
  $("#detail").html(raw);

  // 2) 사용자 제어 문자열을 jQuery($str)로 생성 -> HTML로 파싱됨
  const $frag = $(raw);
  $("#detail").append($frag);
});

변경 후

javascript
// 문자열은 텍스트로, 셀렉터는 셀렉터로만 해석 (안전)
// 옵션: Sanitizer 사용 예시 포함
$(function () {
  const raw = $("#card").attr("data-content");

  // (A) 텍스트로 출력
  $("#detail").text(raw); // HTML 재해석 없음

  // (B) HTML이 꼭 필요하다면 Sanitizer 사용
  // const safe = DOMPurify.sanitize(raw, {ALLOWED_TAGS: ['b','i'], ALLOWED_ATTR: []});
  // $('#detail').html(safe);

  // (C) 사용자 제어 셀렉터는 $() 대신 $.find 사용
  const sel = $("#btn").attr("data-target");
  // 허용 목록 또는 패턴 검증 예시
  const allowed = ["#info", ".section"];
  if (allowed.includes(sel)) {
    $.find(sel).forEach(function (el) {
      el.classList.add("active");
    });
  }
});

설명:

  • 변경 전: attr로 읽은 사용자 제어 문자열(raw)을 .html()이나 jQuery($raw)에 넘기면 브라우저가 이를 HTML로 파싱합니다. 공격자가 이벤트 핸들러를 포함한 악성 마크업을 주입하면 이벤트 핸들러가 실행되어 XSS가 발생합니다.
  • 변경 후: 텍스트는 .text()로 출력해 HTML 재해석을 차단하고, HTML 삽입이 필요할 때는 DOMPurify로 정화합니다. 사용자 제어 셀렉터는 $.find로만 처리하고 허용 목록으로 제한하여 jQuery의 HTML 파싱 경로를 피합니다.

참조