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

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

Jack J
Jack J

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

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

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

사용자 입력 이스케이프

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

const user_input = `<em>hello world</em><img src="" onerro>r=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="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o world/em>img src=""`

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

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

Sanitizer API

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

const $div = document.querySelector('div')
const user_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg src=""/div

하지만 { sanitizer: new Sanitizer() }이 기본 인수입니다. 따라서 아래와 같을 수 있습니다.

$div.setHTML(user_input) // <div><em>hello world</em><img src=&q><uot;>&quot;/div

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

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

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g 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" <]})> })
//< >divhe<ll><o bw>orld/b/div

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div

$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div

다음 옵션을 사용하여 소독기에서 지정된 속성을 허용할지 거부할지 제어할 수도 있습니다.

  • allowAttributes
  • dropAttributes

allowAttributesdropAttributes 속성은 속성 일치 목록을 예상합니다. 속성 일치 목록은 키가 속성 이름이고 값이 타겟 요소 목록 또는 * 와일드카드인 객체입니다.

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

$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div

$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div

$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<

$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div

$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// divhello/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 >})
//< divcustom-e><lemh>ello/custom-elem/div

API 노출 영역

DomPurify와의 비교

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

const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o world/em>img src=""`

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

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를 사용 설정하는 방법

Browser Support

  • Chrome: not supported.
  • Edge: not supported.
  • Firefox Technology Preview: supported.
  • Safari: not supported.

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 제공)