애플리케이션은 항상 신뢰할 수 없는 문자열을 처리하지만 HTML 문서의 일부로 해당 콘텐츠를 안전하게 렌더링하는 것은 까다로울 수 있습니다. 충분히 주의하지 않으면 악의적인 공격자가 악용할 수 있는 교차 사이트 스크립팅 (XSS) 기회를 실수로 만들 수 있습니다.
이러한 위험을 완화하기 위해 새로운 Sanitizer 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>
다음 옵션을 사용하여 새니타이저가 지정된 속성을 허용할지 또는 거부할지 제어할 수도 있습니다.
allowAttributesdropAttributes
allowAttributes 및 dropAttributes 속성은 속성 일치 목록(키가 속성 이름이고 값이 대상 요소 목록 또는 * 와일드카드인 객체)을 예상합니다.
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를 사용 설정하는 방법
Browser Support
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 Playground를 Mike West에서 확인하세요.