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

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

브라우저 지원

  • Chrome: 지원되지 않음 <ph type="x-smartling-placeholder">
  • Edge: 지원되지 않음 <ph type="x-smartling-placeholder">
  • Firefox: 깃발 뒤쪽에 있습니다.
  • Safari: 지원되지 않음 <ph type="x-smartling-placeholder">

소스

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 플레이그라운드를 확인하세요.

참조


사진: Towfiqu barbhuiya, Unsplash