Sanitizer API를 사용한 안전한 DOM 조작

새로운 Sanitizer API는 임의의 문자열을 페이지에 안전하게 삽입할 수 있는 강력한 프로세서를 빌드하는 것을 목표로 합니다.

Jack J
Jack J

애플리케이션은 신뢰할 수 없는 문자열을 항상 처리하지만, 해당 콘텐츠를 HTML 문서의 일부로 안전하게 렌더링하는 것은 까다로울 수 있습니다. 충분한 주의가 없으면 악의적인 공격자가 악용할 수 있는 교차 사이트 스크립팅 (XSS) 기회를 실수로 만들기 쉽습니다.

이러한 위험을 완화하기 위해 새로운 Sanitizer API 제안서는 페이지에 임의의 문자열을 안전하게 삽입할 수 있는 강력한 프로세서를 빌드하는 것을 목표로 합니다. 이 도움말에서는 API를 소개하고 사용법을 설명합니다.

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())

사용자 입력 이스케이프

사용자 입력, 쿼리 문자열, 쿠키 콘텐츠 등을 DOM에 삽입할 때 문자열이 올바르게 이스케이프되어야 합니다. .innerHTML를 통한 DOM 조작에 특히 주의해야 합니다. 여기서 이스케이프 처리되지 않은 문자열은 일반적인 XSS 소스입니다.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.innerHTML = user_input

위 입력 문자열에서 HTML 특수문자를 이스케이프 처리하거나 .textContent를 사용하여 확장하면 alert(0)가 실행되지 않습니다. 그러나 사용자가 추가한 <em>도 그대로 문자열로 확장되므로 이 메서드를 사용하여 HTML의 텍스트 장식을 유지할 수 없습니다.

이때 해야 할 가장 좋은 작업은 이스케이프 처리가 아니라 정리하는 것입니다.

사용자 입력 삭제

이스케이프 처리와 정리의 차이점

이스케이프란 특수 HTML 문자를 HTML 엔티티로 대체하는 것을 의미합니다.

삭제란 HTML 문자열에서 의미론적으로 유해한 부분 (예: 스크립트 실행)을 제거하는 것을 말합니다.

이전 예에서 <img onerror>를 사용하면 오류 핸들러가 실행되지만 onerror 핸들러가 삭제된 경우 <em>는 그대로 두고 DOM에서 안전하게 확장할 수 있습니다.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

올바르게 정리하려면 입력 문자열을 HTML로 파싱하고, 유해한 것으로 간주되는 태그와 속성을 생략하며, 무해한 태그를 유지해야 합니다.

제안된 Sanitizer API 사양의 목표는 이러한 처리를 표준 API로 브라우저에 제공하는 것입니다.

새니타이저 API

Sanitizer API는 다음과 같은 방식으로 사용됩니다.

const $div = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.setHTML(user_input, { sanitizer: new Sanitizer() }) // <div><em>hello world</em><img src=""></div>

그러나 { sanitizer: new Sanitizer() }가 기본 인수입니다. 따라서 아래와 같을 수 있습니다.

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

setHTML()Element에 정의되어 있습니다. Element의 메서드이므로 파싱할 컨텍스트는 설명이 필요하지 않으며 (이 경우 <div>) 파싱이 내부적으로 한 번 이루어지고 결과가 DOM으로 직접 확장됩니다.

정리 결과를 문자열로 가져오려면 setHTML() 결과에서 .innerHTML를 사용하면 됩니다.

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

구성을 통해 맞춤설정

Sanitizer API는 기본적으로 스크립트 실행을 트리거하는 문자열을 삭제하도록 구성되어 있습니다. 그러나 구성 객체를 통해 삭제 프로세스에 자체 맞춤설정을 추가할 수도 있습니다.

const config = {
  allowElements: [],
  blockElements: [],
  dropElements: [],
  allowAttributes: {},
  dropAttributes: {},
  allowCustomElements: true,
  allowComments: true
};
// sanitized result is customized by configuration
new Sanitizer(config)

다음 옵션은 제거 결과가 지정된 요소를 처리하는 방법을 지정합니다.

allowElements: 새니타이저에서 유지해야 하는 요소의 이름입니다.

blockElements: 새니타이저가 하위 요소를 유지하면서 삭제해야 하는 요소의 이름입니다.

dropElements: 새니타이저에서 삭제해야 하는 요소의 이름과 하위 요소입니다.

const str = `hello <b><i>world</i></b>`

$div.setHTML(str)
// <div>hello <b><i>world</i></b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: [ "b" ]}) })
// <div>hello <b>world</b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ "b" ]}) })
// <div>hello <i>world</i></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: []}) })
// <div>hello world</div>

다음 옵션을 사용하여 새니타이저가 지정된 속성을 허용하거나 거부할지 여부를 제어할 수도 있습니다.

  • allowAttributes
  • dropAttributes

allowAttributesdropAttributes 속성에는 속성 일치 목록(키가 속성 이름이고 값이 타겟 요소 또는 * 와일드 카드 목록)이 필요합니다.

const str = `<span id=foo class=bar style="color: red">hello</span>`

$div.setHTML(str)
// <div><span id="foo" class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["span"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["p"]}}) })
// <div><span>hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["*"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({dropAttributes: {"id": ["span"]}}) })
// <div><span class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// <div>hello</div>

allowCustomElements는 맞춤 요소를 허용하거나 거부하는 옵션입니다. 이 속성이 허용되면 요소 및 속성의 다른 구성이 계속 적용됩니다.

const str = `<custom-elem>hello</custom-elem>`

$div.setHTML(str)
// <div></div>

const sanitizer = new Sanitizer({
  allowCustomElements: true,
  allowElements: ["div", "custom-elem"]
})
$div.setHTML(str, { sanitizer })
// <div><custom-elem>hello</custom-elem></div>

API 노출 영역

DomPurify와 비교

DOMPurify는 정리 기능을 제공하는 잘 알려진 라이브러리입니다. Sanitizer API와 DOMPurify의 주요 차이점은 DOMPurify가 삭제 결과를 문자열로 반환하므로 .innerHTML를 통해 DOM 요소에 작성해야 한다는 점입니다.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `<em>hello world</em><img src="">`

Sanitizer API가 브라우저에 구현되지 않은 경우 DOMPurify가 대체 역할을 할 수 있습니다.

DOMPurify 구현에는 몇 가지 단점이 있습니다. 문자열이 반환되면 입력 문자열이 DOMPurify 및 .innerHTML에 의해 두 번 파싱됩니다. 이러한 이중 파싱은 처리 시간을 낭비하지만, 두 번째 파싱의 결과가 첫 번째 구문과 다른 경우 이로 인해 발생하는 흥미로운 취약점이 발생할 수 있습니다.

HTML은 컨텍스트도 파싱해야 합니다. 예를 들어 <td><table>에서는 유용하지만 <div>에서는 그렇지 않습니다. DOMPurify.sanitize()는 문자열만 인수로 취하므로 파싱 컨텍스트를 추측해야 했습니다.

Sanitizer API는 DOMPurify 접근 방식을 개선하며 이중 파싱의 필요성을 없애고 파싱 컨텍스트를 명확히 하도록 설계되었습니다.

API 상태 및 브라우저 지원

Sanitizer API는 표준화 프로세스에서 논의 중이며 Chrome은 이를 구현하는 중입니다.

단계 상태
1. 설명 만들기 완전함
2. 사양 초안 만들기 완전함
3. 의견 수집 및 디자인 반복 완전함
4. Chrome 오리진 트라이얼 완전함
5. 출시 M105 배송 예정

Mozilla: 이 제안서가 프로토타입을 제작할 가치가 있다고 생각하여 적극적으로 구현하고 있습니다.

WebKit: WebKit 메일링 리스트에서 응답을 확인합니다.

Sanitizer API를 사용 설정하는 방법

브라우저 지원

  • x
  • x
  • x

소스

about://flags 또는 CLI 옵션을 통해 사용 설정

Chrome

Chrome에서 Sanitizer API를 구현하는 중입니다. Chrome 93 이상에서는 about://flags/#enable-experimental-web-platform-features 플래그를 사용 설정하여 이러한 동작을 시도해 볼 수 있습니다. 이전 버전의 Chrome Canary 및 개발자 채널에서는 --enable-blink-features=SanitizerAPI를 통해 사용 설정하고 지금 바로 사용해 볼 수 있습니다. 플래그를 사용해 Chrome을 실행하는 방법에 관한 안내를 확인하세요.

Firefox

Firefox는 Sanitizer API도 실험용 기능으로 구현합니다. 사용 설정하려면 about:config에서 dom.security.sanitizer.enabled 플래그를 true로 설정합니다.

기능 감지

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

의견

이 API를 사용해 보시고 의견이 있으면 알려주세요. Sanitizer API GitHub 문제에 대한 의견을 공유하고 사양 작성자 및 이 API에 관심이 있는 사람들과 논의하세요.

Chrome 구현에서 버그나 예상치 못한 동작을 발견하면 버그를 제출하여 신고하세요. 구현자가 문제를 추적할 수 있도록 Blink>SecurityFeature>SanitizerAPI 구성요소를 선택하고 세부정보를 공유합니다.

데모

Sanitizer API의 실제 동작을 보려면 Mike WestSanitizer API Playground를 확인하세요.

참조


사진: Towfiqu barbhuiya, Unsplash 제공