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에 삽입할 때는 문자열을 올바르게 이스케이프 처리해야 합니다. 이스케이프되지 않은 문자열이 XSS의 일반적인 소스인 .innerHTML를 통한 DOM 조작에 특히 주의해야 합니다.

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로 제공하는 것을 목표로 합니다.

Sanitizer 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="">`

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

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가 작동하는 모습을 보려면 마이크 웨스트Sanitizer API 플레이그라운드를 확인하세요.

참조


사진: UnsplashTowfiqu barbhuiya