Bezpieczne manipulacje DOM za pomocą interfejsu Sanitizer API

Nowy interfejs Sanitizer API ma na celu stworzenie niezawodnego procesora dowolnych ciągów znaków, które można bezpiecznie wstawiać na stronę.

Jack J
Jack J

Aplikacje często mają do czynienia z niezaufanymi ciągami znaków, ale bezpieczne renderowanie tych treści w ramach dokumentu HTML może być trudne. Jeśli nie zachowasz ostrożności, łatwo możesz przypadkowo stworzyć możliwości cross-site scripting (XSS), które mogą wykorzystać złośliwi hakerzy.

Aby zmniejszyć to ryzyko, nowa propozycja interfejsu Sanitizer API ma na celu stworzenie niezawodnego procesora dowolnych ciągów znaków, które można bezpiecznie wstawiać na stronę. Z tego artykułu dowiesz się, czym jest ten interfejs API i jak go używać.

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

Unikanie danych wejściowych użytkownika

Podczas wstawiania do DOM danych wprowadzonych przez użytkownika, ciągów zapytania, zawartości plików cookie itp. ciągi znaków muszą być odpowiednio zmienione. Szczególną uwagę należy zwrócić na manipulowanie DOM za pomocą .innerHTML, gdzie niekodowane ciągi znaków są typowym źródłem ataków XSS.

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

Jeśli w ciągu wejściowym powyżej zastosujesz znaki specjalne HTML lub rozwiniesz go za pomocą .textContent, funkcja alert(0) nie zostanie wykonana. Jednak ponieważ znak <em> dodany przez użytkownika jest również rozwijany jako ciąg znaków, tej metody nie można użyć, aby zachować dekorację tekstu w HTML.

Najlepszym rozwiązaniem w tym przypadku nie jest ucieczka, ale oczyszczenie.

Usuwanie informacji z danych wejściowych użytkownika

Różnica między unikaniem znaków specjalnych a oczyszczaniem danych

Ucieczka polega na zastąpieniu specjalnych znaków HTML encjami HTML.

Oczyszczanie polega na usuwaniu z ciągów HTML części, które mogą być szkodliwe semantycznie (np. wykonywanie skryptów).

Przykład

W poprzednim przykładzie <img onerror> powoduje wykonanie procedury obsługi błędów, ale jeśli procedura obsługi onerror zostanie usunięta, można bezpiecznie rozwinąć element w DOM, pozostawiając element <em> bez zmian.

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

Aby prawidłowo oczyścić ciąg znaków, należy przeanalizować go jako kod HTML, pominąć tagi i atrybuty, które są uważane za szkodliwe, i zachować te, które są bezpieczne.

Proponowana specyfikacja interfejsu Sanitizer API ma na celu udostępnienie takiego przetwarzania jako standardowego interfejsu API dla przeglądarek.

Sanitizer API

Interfejs Sanitizer API jest używany w ten sposób:

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

Argumentem domyślnym jest jednak { sanitizer: new Sanitizer() }. Może to być np. tak jak poniżej.

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

Warto zauważyć, że setHTML() jest zdefiniowane w Element. Jako metoda Element kontekst do analizy jest oczywisty (w tym przypadku <div>), analiza jest wykonywana wewnętrznie tylko raz, a wynik jest bezpośrednio rozwijany w DOM.

Aby uzyskać wynik czyszczenia w postaci ciągu tekstowego, możesz użyć .innerHTML z wyników setHTML().

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g src=""

Dostosowywanie za pomocą konfiguracji

Interfejs Sanitizer API jest domyślnie skonfigurowany tak, aby usuwać ciągi znaków, które mogłyby uruchomić skrypt. Możesz jednak dodać własne dostosowania do procesu czyszczenia za pomocą obiektu konfiguracji.

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

Poniższe opcje określają, jak wynik czyszczenia ma traktować określony element.

allowElements: nazwy elementów, które narzędzie do czyszczenia powinno zachować.

blockElements: nazwy elementów, które narzędzie do czyszczenia powinno usunąć, zachowując ich elementy podrzędne.

dropElements: nazwy elementów, które narzędzie do czyszczenia powinno usunąć wraz z elementami podrzędnymi.

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

Możesz też określić, czy narzędzie do czyszczenia ma zezwalać na określone atrybuty, czy je odrzucać, korzystając z tych opcji:

  • allowAttributes
  • dropAttributes

Właściwości allowAttributesdropAttributes oczekują list dopasowania atrybutów – obiektów, których klucze są nazwami atrybutów, a wartości są listami elementów docelowych lub symbolem wieloznacznym *.

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 to opcja zezwalająca na elementy niestandardowe lub zabraniająca ich stosowania. Jeśli są dozwolone, nadal obowiązują inne konfiguracje elementów i atrybutów.

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

Powierzchnia interfejsu API

Porównanie z DomPurify

DOMPurify to znana biblioteka, która oferuje funkcję oczyszczania. Główna różnica między Sanitizer API a DOMPurify polega na tym, że DOMPurify zwraca wynik oczyszczania jako ciąg znaków, który musisz zapisać w elemencie DOM za pomocą .innerHTML.

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 może służyć jako rozwiązanie rezerwowe, gdy interfejs Sanitizer API nie jest zaimplementowany w przeglądarce.

Implementacja DOMPurify ma kilka wad. Jeśli zwracany jest ciąg znaków, ciąg wejściowy jest analizowany dwukrotnie przez DOMPurify i .innerHTML. Podwójne parsowanie marnuje czas przetwarzania, ale może też prowadzić do ciekawych luk w zabezpieczeniach, gdy wynik drugiego parsowania różni się od pierwszego.

HTML wymaga też kontekstu do przetworzenia. Na przykład reguła <td> ma zastosowanie do witryny <table>, ale do witryny <div> już nie. Funkcja DOMPurify.sanitize() przyjmuje tylko ciąg znaków jako argument, więc kontekst analizowania musiał zostać odgadnięty.

Sanitizer API jest ulepszoną wersją DOMPurify. Został zaprojektowany tak, aby wyeliminować konieczność podwójnego parsowania i wyjaśnić kontekst parsowania.

Stan interfejsu API i obsługa przeglądarek

Interfejs Sanitizer API jest obecnie omawiany w ramach procesu standaryzacji, a Chrome jest w trakcie jego wdrażania.

Krok Stan
1. Tworzenie wyjaśnienia Zakończono
2. Tworzenie wersji roboczej specyfikacji Zakończono
3. Zbieranie opinii i ulepszanie projektu Zakończono
4. Testowanie origin w Chrome Zakończono
5. Uruchom Zamiar wysyłki w przypadku M105

Mozilla: uważa, że ten pomysł warto przetestować, i aktywnie go wdraża.

WebKit: odpowiedź znajdziesz na liście mailingowej WebKit.

Jak włączyć interfejs Sanitizer API

Browser Support

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

Włączanie za pomocą opcji about://flags lub interfejsu wiersza poleceń

Chrome

Chrome wdraża interfejs Sanitizer API. W Chrome w wersji 93 lub nowszej możesz wypróbować to zachowanie, włączając flagę about://flags/#enable-experimental-web-platform-features. W starszych wersjach Chrome Canary i na kanale deweloperskim możesz włączyć tę funkcję za pomocą ikony --enable-blink-features=SanitizerAPI i od razu ją wypróbować. Zapoznaj się z instrukcjami uruchamiania Chrome z flagami.

Firefox

Firefox również implementuje interfejs Sanitizer API jako funkcję eksperymentalną. Aby ją włączyć, ustaw flagę dom.security.sanitizer.enabled na trueabout:config.

Wykrywanie cech

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

Prześlij opinię

Jeśli wypróbujesz ten interfejs API i chcesz podzielić się z nami opinią, chętnie ją poznamy. Podziel się swoimi przemyśleniami na temat problemów z interfejsem Sanitizer API w GitHubie i porozmawiaj z autorami specyfikacji oraz osobami zainteresowanymi tym interfejsem API.

Jeśli w implementacji Chrome znajdziesz błędy lub nieoczekiwane zachowania, zgłoś je. Wybierz komponenty Blink>SecurityFeature>SanitizerAPI i udostępnij szczegóły, aby pomóc osobom wdrażającym śledzić problem.

Prezentacja

Aby zobaczyć działanie interfejsu Sanitizer API, odwiedź Sanitizer API Playground (w języku angielskim) stworzony przez Mike’a Westa:

Odniesienia


Zdjęcie: Towfiqu barbhuiya, Unsplash