신뢰할 수 없는 HTML 정리

웹 기능: Sanitizer API

사용자 제작 HTML을 안전하게 표시하는 것은 일반적인 보안 과제입니다. 네이티브 Sanitizer API는 위험한 콘텐츠 (예: <script> 태그 또는 on* 속성)가 DOM에 도달하기 전에 삭제하는 브라우저 최적화 기본 제공 방법을 제공합니다.

클라이언트 측 정제는 콘텐츠를 렌더링하는 페이지를 보호합니다. 동일한 사용자 제작 콘텐츠가 다른 곳 (다른 클라이언트, 서버에서 렌더링된 페이지 또는 이메일)에서도 렌더링되는 경우 심층 방어와 마찬가지로 서버에서 정제하고 서버와 클라이언트 정제 규칙이 충돌하지 않는지 확인합니다.

1. setHTML()를 사용하여 안전하게 HTML 삽입

Sanitizer API를 사용하는 기본 방법은 Element의 setHTML() 메서드를 사용하는 것입니다. 이 메서드는 파싱, 정제, 삽입을 한 단계로 처리하여 콘텐츠가 삽입될 컨텍스트에 맞게 정제되도록 합니다.

const untrustedHTML = `
  <p>Hello!</p>
  <script>alert('XSS')</script>
  <img src="x" onerror="alert('XSS')">
  <div onclick="doBadThing()">Click me</div>
`;

const container = document.getElementById('output');

// Sanitize and insert in one call
// The default sanitizer removes scripts and event handlers, plus other
// elements and attributes outside its built-in allowlist (such as style)
container.setHTML(untrustedHTML);

2. 커스텀 새니타이저 구성 사용

Sanitizer 인스턴스를 만들어 살균 규칙을 맞춤설정할 수 있습니다. 이를 통해 특정 사용 사례에 허용되는 요소와 속성을 정확하게 정의할 수 있습니다.

Element.setHTML() 및 Document.parseHTML()를 사용하는 경우 기본 Sanitizer 설정은 모든 XSS 안전하지 않은 요소와 속성을 삭제합니다. 이러한 요소를 허용하는 더 허용적인 Sanitizer를 만들 수는 없지만 허용되는 항목을 더 제한할 수는 있습니다.

// Define a restrictive configuration that only allows the specified elements and attributes.
const config = {
  elements: ["p", "b", "i", "strong", "em"],
  attributes: ["class"],
  replaceWithChildrenElements: ['div']
};

const mySanitizer = new Sanitizer(config);

// Apply custom sanitization
container.setHTML(untrustedHTML, { sanitizer: mySanitizer });

elements에 없는 요소는 콘텐츠와 함께 삭제됩니다. 텍스트를 유지하려는 래퍼 요소 (예: 위의 div)를 replaceWithChildrenElements에 나열합니다. 그러면 요소가 삭제되지만 하위 요소는 유지됩니다.

일부 XSS 안전하지 않은 요소나 속성을 허용하는 더 허용적인 Sanitizer가 필요한 경우 Element.setHTMLUnsafe() 및 Document.parseHTMLUnsafe()와 함께 맞춤 Sanitizer를 사용할 수 있습니다.

3. Document.parseHTML()로 사용자 입력 파싱

신뢰할 수 없는 HTML을 라이브 DOM에 즉시 삽입하지 않고 파싱해야 하는 경우 Document.parseHTML()를 사용하세요. 이 메서드는 기본 또는 맞춤 규칙에 따라 정제된 Document를 반환하므로 정제된 노드를 삽입하기 전에 검사하거나 변환할 수 있습니다 (예: 미리보기를 빌드하거나 텍스트를 추출하기 위해).

결과 노드를 문자열로 직렬화하고 innerHTML로 다시 파싱하는 대신 페이지로 이동합니다. 이렇게 하면 변형 XSS (mXSS)가 다시 발생할 수 있습니다.

const rawHTML = '<p>Hello <script>console.log("bad")</script></p>';

const doc = Document.parseHTML(rawHTML);
// doc.body now contains the sanitized nodes: <p>Hello </p>
preview.replaceChildren(...doc.body.childNodes);

Sanitizer API의 대체 및 브라우저 지원

타겟 브라우저에서 네이티브 Sanitizer API를 사용할 수 없는 경우 네이티브 지원이 누락된 경우에만 DOMPurify와 같은 라이브러리를 로드하여 필요하지 않은 브라우저에 전송하지 않도록 합니다. 기능 감지에 in 검사를 사용하면 검사 자체가 이전 브라우저에서 작동합니다.

let domPurifyPromise;

function loadDOMPurify() {
  // Code-split with your bundler so DOMPurify is only downloaded when needed
  domPurifyPromise ??= import('dompurify').then((mod) => mod.default ?? mod);
  return domPurifyPromise;
}

export async function safeSetHTML(el, html) {
  const elements = ['p', 'b', 'i', 'strong', 'em'];
  const attributes = ['class'];

  if ('setHTML' in Element.prototype) {
    // setHTML() accepts a plain configuration object as the sanitizer
    el.setHTML(html, { sanitizer: { elements, attributes } });
  } else {
    const DOMPurify = await loadDOMPurify();
    el.innerHTML = DOMPurify.sanitize(html, {
      ALLOWED_TAGS: [...elements, '#text'],
      ALLOWED_ATTR: attributes,
      // Match the native Sanitizer: strip aria-*/data-* and disallowed element content
      ALLOW_ARIA_ATTR: false,
      ALLOW_DATA_ATTR: false,
      KEEP_CONTENT: false
    });
  }
}

기본적으로 DOMPurify는 aria-* 및 data-* 속성(ALLOWED_ATTR가 지정된 경우에도)과 삭제하는 요소의 텍스트 콘텐츠를 유지하는 반면, 네이티브 Sanitizer는 aria-* 및 data-* 속성을 제거하고 허용되지 않는 요소를 콘텐츠와 함께 삭제합니다. ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false, KEEP_CONTENT: false를 설정하고 #text를 허용하면 DOMPurify가 네이티브 동작과 일치하게 됩니다. DOMPurify에는 replaceWithChildrenElements에 상응하는 기본 제공 옵션이 없으므로 일치하는 대체가 필요한 구성에서는 이 옵션을 사용하지 마세요.

페이지에서 신뢰할 수 있는 유형 (require-trusted-types-for 'script')을 적용하는 경우 innerHTML에 문자열을 할당하면 오류가 발생합니다. setHTML()에는 신뢰할 수 있는 유형 정책이 필요하지 않지만 대체에는 필요합니다. RETURN_TRUSTED_TYPE: true를 DOMPurify.sanitize()에 전달하고 trusted-types CSP 지시어에서 dompurify 정책 이름을 허용하세요.