Element.setHTML(): XSS-safe한 innerHTML 대체품

okorion·2025년 11월 14일

브라우저에 Element.setHTML() 이라는 새로운 메서드가 들어왔다.
역할은 단순하다.

“문자열 HTML을 파싱 + Sanitizing 해서, XSS-safe한 DOM 서브트리로 삽입하는 메서드”

실제 의도는 더 명확하다.

“사용자가 제공한 HTML 문자열을 넣을 때 innerHTML 대신 쓰는 안전한 API”

단, 실험적(Experimental) 기능이고, 브라우저 지원도 아직 제한적이기 때문에, 실제 프로덕션에서는 반드시 호환성 테이블을 확인해야 한다.


1. setHTML() 개요

1.1 메서드 시그니처

element.setHTML(input);
element.setHTML(input, options);
  • input: 문자열 HTML
  • options (선택)
    • sanitizer:
      • Sanitizer 인스턴스
      • 혹은 SanitizerConfig 객체
      • 혹은 "default" 문자열

반환값

  • undefined (반환값 없음)

예외

TypeError가 발생하는 경우:

  • options.sanitizer비정상 SanitizerConfig 인 경우
    • 예: allowedremoved(혹은 allow/remove 계열 옵션)를 동시에 섞어쓴 설정
  • 문자열인데 "default" 가 아닌 값인 경우
  • Sanitizer, SanitizerConfig, 문자열, 어느 쪽에도 해당하지 않는 타입인 경우

2. setHTML() 동작 방식

2.1 전체 플로우

setHTML()은 대략 다음 순서로 동작한다고 볼 수 있다.

  1. HTML 문자열 파싱 → DocumentFragment 생성
  2. 현재 요소 컨텍스트에서 유효하지 않은 요소 제거
    • 예: <table> 밖에 있는 <col> 같은 것
  3. 전달된 sanitizer(혹은 기본 sanitizer) 설정에 따라
    • 허용/제거 대상 요소, 속성, 주석 등을 필터링
  4. 그 후 추가로 XSS-unsafe 요소/속성을 강제로 제거
    • sanitizer가 허용하더라도 무조건 제거
  5. 정제된 DocumentFragment를 element의 자식 서브트리로 삽입

핵심: “항상 XSS-safe를 우선” 한다.

2.2 항상 제거되는 요소/속성

문자열에 아래 요소들이 있어도, 최종 DOM에는 들어가지 않는다.

  • <script>
  • <frame>
  • <iframe>
  • <embed>
  • <object>
  • <use>
  • 모든 이벤트 핸들러 속성 (onclick, onmouseover 등)

중요한 점:

sanitizer로 저 요소들을 “허용”하더라도, setHTML()강제로 제거한다.

이는 내부적으로 Sanitizer.removeUnsafe() 를 항상 거치는 것과 같다.


3. Sanitizer / SanitizerConfig와의 관계

3.1 기본 Sanitizer

options.sanitizer를 생략하면 기본 Sanitizer 설정을 사용한다.

target.setHTML(unsafeString);

기본 설정의 의미:

  • “XSS-safe하다고 판단되는 요소와 속성은 허용”
  • Unsafe하다고 알려진 요소/속성은 제거
    보안 우선 정책

자세한 구성은 Sanitizer() 생성자 문서를 기준으로 한다.

3.2 Custom Sanitizer (인스턴스)

const sanitizer = new Sanitizer({
  elements: ["div", "p", "button", "script"], // script를 허용하려 해도…
});

target.setHTML(unsafeString, { sanitizer });
  • 이 예시에서 Sanitizer는 script를 허용하도록 구성됐지만,
  • setHTML()추가로 XSS-unsafe 요소를 제거 하므로
    • script는 결국 DOM에 삽입되지 않는다.

3.3 Custom SanitizerConfig (plain object)

target.setHTML(unsafeString, {
  sanitizer: {
    removeElements: ["div", "p", "button", "script"],
  },
});
  • SanitizerConfig를 직접 넘기면, 내부적으로 Sanitizer를 생성해서 사용
  • removeElements 등을 이용해 요소 제거 정책을 커스터마이징
  • 여전히 unsafe 요소/속성은 무조건 제거된다.

3.4 Sanitizer vs SanitizerConfig 선택 기준

  • Sanitizer 인스턴스 재사용
    • 여러 번 같은 설정으로 sanitize해야 할 때
    • 성능상 유리 (구성이 캐시됨)
  • SanitizerConfig
    • 한 번만 쓰는 간단한 설정이라면 inline으로 넘겨도 됨

4. innerHTML vs setHTML

4.1 innerHTML의 문제

innerHTML은 단순 문자열 파서 + DOM 삽입 메서드다.

element.innerHTML = userInput; // XSS risk!
  • 문자열이 그대로 파싱되어 DOM에 들어간다.
  • 스크립트 태그, 이벤트 핸들러, 인라인 JS 등 모두 실행/노출 가능
  • 따라서 사용자 입력 또는 외부 소스 기반 HTML에 직접 쓰는 것은 위험하다.

4.2 setHTML의 의도

“사용자 제공 HTML을 삽입해야 한다면, 가능하면 innerHTML 대신 setHTML()을 쓰라.”

특히:

  • 댓글, WYSIWYG 편집기, 마크다운 렌더링 결과, 외부 CMS HTML 등
  • XSS에 취약한 경로에서 강제 Sanitizing 레이어 역할을 한다.

4.3 setHTMLUnsafe()와의 관계

  • Element.setHTMLUnsafe()라는 메서드도 언급된다.
    • 이름 그대로 unsafe 요소/속성까지 허용하는 버전
  • 특별한 이유로 의도적으로 unsafe HTML을 허용해야 할 때만 사용하고,
  • 일반적으로는 무조건 setHTML()을 쓰라는 것이 권장 사항이다.

5. 예제 정리

5.1 기본 사용

// 1) 위험한 문자열
const unsafe = "abc <script>alert(1)</script> def";

// 2) 대상 요소
const target = document.getElementById("target");

// 3) 기본 sanitizer 사용
target.setHTML(unsafe);
// 결과: <script> 제거된 문자열만 삽입

5.2 Custom Sanitizer 인스턴스 + Config

// Custom Sanitizer: div, p, button, script 허용이라고 설정했지만
const sanitizer1 = new Sanitizer({
  elements: ["div", "p", "button", "script"],
});

target.setHTML(unsafe, { sanitizer: sanitizer1 });
// → script는 XSS-unsafe라서 여전히 제거됨

// Config를 inline으로 전달해서 특정 요소를 제거하도록 설정
target.setHTML(unsafe, {
  sanitizer: { removeElements: ["div", "p", "button", "script"] },
});
// → div/p/button/script 모두 제거 + 그 외 unsafe도 제거

5.3 “라이브” 예제 구조 요약

HTML

<button id="buttonDefault" type="button">Default</button>
<button id="buttonAllowScript" type="button">allowScript</button>

<button id="reload" type="button">Reload</button>
<div id="target">Original content of target element</div>

공통 JS

const unsanitizedString = `
  <div>
    <p>Paragraph to inject into shadow DOM.
      <button>Click me</button>
    </p>
    <script src="path/to/a/module.js" type="module"></script>
    <p data-id="123">Para with <code>data-</code> attribute</p>
  </div>
`;

const target = document.querySelector("#target");
const logElement = document.getElementById("log"); // 예: 로그 출력 영역

const reload = document.querySelector("#reload");
reload.addEventListener("click", () => document.location.reload());

1) 기본 Sanitizer 버튼

const defaultSanitizerButton = document.querySelector("#buttonDefault");

defaultSanitizerButton.addEventListener("click", () => {
  target.setHTML(unsanitizedString);

  logElement.textContent =
    "Default sanitizer: remove script element, onclick attribute, data- attribute\n\n";
  log(`\nunsanitized: ${unsanitizedString}`);
  log(`\n\nsanitized: ${target.innerHTML}`);
});

효과:

  • <script> 제거
  • onclick 속성 제거
  • data-id 속성도 제거 (기본 설정 기준, unsafe로 분류)

2) Custom Sanitizer 버튼

const allowScriptButton = document.querySelector("#buttonAllowScript");
allowScriptButton.addEventListener("click", () => {
  const sanitizer1 = new Sanitizer({
    elements: ["div", "p", "script"],
  });

  target.setHTML(unsanitizedString, { sanitizer: sanitizer1 });

  logElement.textContent =
    "Sanitizer: {elements: ['div', 'p', 'script']}\n Script removed even though allowed\n";
  log(`\nunsanitized: ${unsanitizedString}`);
  log(`\n\nsanitized: ${target.innerHTML}`);
});

효과:

  • Sanitizer는 script를 허용하지만
    setHTML()이 XSS-unsafe로 판단해 <script>는 여전히 제거
  • data-id 는, 이 경우엔 custom sanitizer 설정 상 허용될 수 있다
    (기본 sanitizer와 비교 시, 차이가 나는 포인트)

6. Trusted Types와의 관계

  • setHTML()항상 자체적으로 sanitize를 수행한다.
  • 따라서 Trusted Types API와 별도 연동/검증을 하지 않는다.
    • 즉, TrustedHTML 같은 타입을 요구하지도 않고,
    • Trusted Types 정책을 통해 추가 검증을 하는 구조도 아니다.

요약:

“Trusted Types는 innerHTML/insertAdjacentHTML류에 대한 보안 레이어고,
setHTML()은 애초에 XSS-safe 설계라 별도의 Trusted Types 검증을 하지 않는다.”


7. 브라우저 지원 및 사용 시 주의사항

  • setHTML()“Experimental” 로 명시되어 있다.
  • 실제 사용 전에:
    • 브라우저 호환성 테이블을 반드시 확인해야 한다.
    • 지원하지 않는 브라우저가 있다면:
      • Polyfill 또는
      • innerHTML + 별도 Sanitizer 라이브러리 (DOMPurify 등) 조합을 고려

실무 가이드라인:

  1. 실험/내부 도구/최신 브라우저 타겟
    • setHTML() 적극 테스트 가능
    • Sanitizer API와 함께 사용하는 패턴 익히기
  2. 대규모 상용 서비스(광범위 브라우저 지원 필요)
    • 현재 시점에서는 폴백 전략과 함께만 사용 고려
    • 최소 지원 브라우저 정책과 맞춰서 결정

8. 요약

  • Element.setHTML()XSS-safe한 innerHTML 대체 API다.
  • 문자열 HTML을 파싱 후:
    • 컨텍스트에 맞지 않는 요소 제거
    • Sanitizer 설정에 따라 허용/제거
    • 추가로 XSS-unsafe 요소/속성은 무조건 제거
  • <script>, <iframe>, <object>, <embed>, <use>, 이벤트 핸들러 속성 등은 항상 제거된다.
  • Sanitizer / SanitizerConfig 옵션으로 세부 정책을 설정할 수 있지만,
    • 보안 관련 unsafe 옵션은 강제로 제거되는 것이 전제.
  • 사용자로부터 받은 HTML을 DOM에 넣어야 한다면:
    • 가능하면 innerHTML 대신 setHTML() 사용을 권장.
  • 아직 실험적 기능이므로, 실제 서비스에서는 브라우저 지원 상황을 반드시 확인한 뒤 사용해야 한다.

profile
okorion's Tech Study Blog.

0개의 댓글