클라이언트 측 DOM 기반 XSS

DOM-based Cross-Site Scripting (XSS)

설명

DOM 기반 XSS는 브라우저 내 JavaScript가 사용자 제어 입력(예: location.search, location.hash, URL 파라미터, 엘리먼트 value 등)을 검증·인코딩 없이 위험한 DOM API(innerHTML, outerHTML, insertAdjacentHTML, document.write)로 삽입할 때 발생합니다. 공격자는 특수 문자를 포함한 페이로드를 URL 등에 넣어 HTML 태그나 이벤트 핸들러를 주입하고, 결과적으로 임의의 JavaScript를 실행시킬 수 있습니다. 예를 들어, ?q=<img src=x onerror=alert(1)> 같은 입력이 innerHTML로 반영되면 스크립트가 실행됩니다. 또한 안전한 텍스트로 얻은 값을 다시 HTML 컨텍스트로 재삽입할 때도(텍스트 → innerHTML 재해석) 취약해질 수 있습니다.

잠재적 영향

  • 임의 스크립트 실행: 공격자가 피해자의 브라우저에서 임의의 JavaScript를 실행
  • 세션/계정 탈취: 스크립트가 읽을 수 있는 쿠키, 토큰, localStorage 등의 민감 데이터 탈취로 계정 접수 가능
  • 데이터 유출 및 변조: 페이지 DOM 조작을 통해 화면 내용 변조, 폼 데이터 가로채기, 내부 정보 수집
  • 사용자 피싱/사기 유도: 가짜 입력창/알림 표시로 사용자 오인 유도 및 추가 공격 연결

해결 방법

  • 텍스트 출력에는 textContent/innerText를 사용하고, innerHTML/outerHTML/document.write/insertAdjacentHTML에 신뢰할 수 없는 입력을 넣지 마세요.
  • 불가피하게 HTML을 삽입해야 한다면 DOMPurify, sanitize-html 등의 검증된 Sanitizer로 정화 후 사용하세요.
  • 컨텍스트별 인코딩 적용: HTML 텍스트, 속성, URL, JavaScript 컨텍스트마다 적절한 이스케이프/인코딩을 적용하세요.
  • 입력 파싱 시 URLSearchParams, new URL() 등 안전한 파서를 사용하고, 필요 시 허용 목록(whitelist) 검증을 적용하세요.
  • 브라우저 보강: CSP(Content-Security-Policy), Trusted Types(지원 환경) 등을 활성화해 XSS 피해 범위를 줄이세요.

예시

변경 전

javascript
// 변경 전: DOM 출력 처리
(function () {
  // URL 해시값을 읽어 DOM에 삽입 (실제 해석되는 값 확인)
  const raw = location.hash.slice(1); // 예: #<img src=x onerror=alert(1)>
  const box = document.getElementById('msg');
  // BAD: 사용자 입력을 HTML 컨텍스트에 직접 삽입
  box.innerHTML = '<p>결과: ' + raw + '</p>';

  // 또 다른 위험: document.write로 사용자 입력 출력
  const q = new URL(location.href).searchParams.get('q') || '';
  document.write('<div>검색어: ' + q + '</div>');
})();

변경 후

javascript
(function () {
  const params = new URL(location.href).searchParams;
  const q = params.get('q') || '';
  const box = document.getElementById('msg');

  // 1) 텍스트 컨텍스트 사용: HTML로 해석되지 않음
  box.textContent = `검색어: ${q}`;

  // 2) 불가피하게 HTML이 필요할 때는 Sanitizer 사용
  // 예: DOMPurify가 전역으로 로드되어 있다고 가정
  const userHtml = params.get('html') || '';
  const safeHtml = DOMPurify.sanitize(userHtml, {ALLOWED_TAGS: ['b','i','u']});
  document.getElementById('preview').innerHTML = safeHtml;

  // 3) URL 등 속성 컨텍스트에는 안전한 조합 사용
  const link = document.getElementById('link');
  const dest = params.get('dest') || '/';
  // 허용 목록 검사 예시
  const allowed = ['/', '/help', '/profile'];
  link.setAttribute('href', allowed.includes(dest) ? dest : '/');
})();

설명:

  • 변경 전: 사용자 제어 값을 HTML로 해석하는 API에 전달합니다. URLSearchParams로 읽은 쿼리 값은 디코딩되지만 location.hash는 읽을 때 퍼센트 디코딩되지 않으므로, 해시에 인코딩된 태그가 있다는 사실만으로 실행된다고 판단하면 안 됩니다. 실제 HTML 값이 만들어지는 경로를 확인하세요.
  • 변경 후: 텍스트 출력에는 textContent를 사용해 HTML로 해석되지 않도록 했고, 불가피한 HTML 삽입은 DOMPurify로 정화하여 악성 태그/속성을 제거합니다. 또한 URL 등 속성 값은 허용 목록을 통해 제한하여 DOM 기반 XSS 가능성을 줄였습니다.

참조