Bezpieczne manipulacje DOM za pomocą interfejsu Sanitizer API

Nowy interfejs Sanitizer API ma na celu stworzenie niezawodnego procesora, który umożliwia bezpieczne wstawianie dowolnych ciągów znaków na stronie.

Jack J
Jack J

Aplikacje stale mają do czynienia z niezaufanymi ciągami znaków, ale bezpieczne renderowanie tych treści w ramach dokumentu HTML może być trudne. Bez odpowiedniej ostrożności łatwo jest przypadkowo stworzyć możliwości cross-site scriptingu (XSS), które mogą wykorzystać złośliwi hakerzy.

Aby zminimalizować to ryzyko, nowa propozycja Sanitizer API ma na celu stworzenie niezawodnego procesora, który umożliwia bezpieczne wstawianie dowolnych ciągów znaków na stronie. Z tego artykułu dowiesz się, czym jest interfejs API i jak z niego korzystać.

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

Umieszczanie znaku ucieczki w danych wejściowych użytkownika

Wstawiając dane użytkownika, ciągi znaków zapytania, zawartość plików cookie itp. do DOM, należy odpowiednio oznaczać znaczenie ciągów znaków. Szczególną uwagę należy zwrócić na manipulowanie DOM za pomocą .innerHTML, gdzie nieotagowane ciągi znaków są typowym źródłem ataków XSS.

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

Jeśli w powyższym ciągu znaków wejściowych użyjesz znaków ucieczki HTML lub rozwiniesz go za pomocą znaku .textContent, funkcja alert(0) nie zostanie wykonana. Jednak ponieważ <em> dodane przez użytkownika jest również rozszerzane jako ciąg znaków, tej metody nie można użyć do zachowania dekoracji tekstu w kodzie HTML.

Najlepiej nie uciekać się do ucieczki, ale do sanityzowania.

Sanitizing user input

Różnica między ucieczką a odkażeniem

Uciekające znaki to znaki specjalne HTML zastępowane przez entyte HTML.

Sanitizing oznacza usuwanie z ciągów HTML elementów, które mogą być szkodliwe semantycznie (np. uruchamianie skryptu).

Przykład

W poprzednim przykładzie element <img onerror> powoduje wykonanie modułu obsługi błędów, ale jeśli moduł obsługi onerror zostanie usunięty, można go bezpiecznie rozwinąć w DOM, nie zmieniając przy tym elementu <em>.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

Aby prawidłowo odizolować dane, należy przeanalizować podany ciąg znaków jako kod HTML, pomijając tagi i atrybuty uważane za szkodliwe, a zachowując te, które są nieszkodliwe.

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_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>

Jednak argument domyślny to { sanitizer: new Sanitizer() }. Może to wyglądać tak jak poniżej.

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

Warto zauważyć, że setHTML() jest zdefiniowana w Element. Ponieważ jest to metoda Element, kontekst do zanalizowania jest oczywisty (w tym przypadku <div>), analiza jest wykonywana raz wewnętrznie, a jej wynik jest bezpośrednio rozszerzany w DOM.

Aby uzyskać wynik odkażania jako ciąg znaków, możesz użyć funkcji .innerHTML z wyników funkcji setHTML().

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

Dostosowywanie za pomocą konfiguracji

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

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

Opcje podane poniżej określają, jak wynik skanowania ma traktować określony element.

allowElements: nazwy elementów, które powinny być zachowane.

blockElements: nazwy elementów, które dezynfekator powinien usunąć, zachowując ich podrzędne elementy.

dropElements: nazwy elementów, które ma usunąć oczyszczacz, wraz z ich 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" ]}) })
// <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>

Możesz też określić, czy oczyszczanie ma zezwalać na określone atrybuty lub je blokować, korzystając z tych opcji:

  • allowAttributes
  • dropAttributes

Właściwości allowAttributesdropAttributes oczekują list dopasowania atrybutów – obiektów, których klucze to nazwy atrybutów, a wartości to listy elementów docelowych lub symbol wieloznaczny *.

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 to opcja zezwalająca na elementy niestandardowe lub ją blokująca. 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 })
// <div><custom-elem>hello</custom-elem></div>

Interfejs API

Porównanie z DomPurify

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

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

Implementacja DOMPurify ma kilka wad. Jeśli zwrócony zostanie ciąg znaków, zostanie on przeanalizowany dwukrotnie: przez DOMPurify i .innerHTML. Podwójne parsowanie zajmuje czas, ale może też prowadzić do ciekawych luk spowodowanych przez przypadki, w których wynik drugiego parsowania różni się od pierwszego.

Do analizy kodu HTML potrzebny jest też kontekst. Na przykład reguła <td> ma sens w przypadku <table>, ale nie <div>. Funkcja DOMPurify.sanitize() przyjmuje jako argument tylko ciąg znaków, więc kontekst analizy musiał być zgadywany.

Interfejs Sanitizer API ulepsza podejście stosowane przez DOMPurify i ma na celu wyeliminowanie potrzeby podwójnego parsowania oraz uściślenie kontekstu parsowania.

Stan interfejsu API i obsługa przeglądarek

Interfejs Sanitizer API jest obecnie omawiany w procesie 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. Zbieraj opinie i ulepszaj projekt Zakończono
4. Testowanie origin w Chrome Zakończono
5. Uruchom Intencja wysyłki w M105

Mozilla: uważa, że to rozwiązanie warto prototypować, i aktywnie je wdraża.

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

Jak włączyć interfejs Sanitizer API

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

Chrome

W Chrome trwa implementacja interfejsu Sanitizer API. W Chrome 93 lub nowszej możesz wypróbować tę funkcję, włączając flagę about://flags/#enable-experimental-web-platform-features. W wcześniejszych wersjach Chrome Canary i na kanale deweloperskim możesz go włączyć za pomocą --enable-blink-features=SanitizerAPI i od razu go wypróbować. Zapoznaj się z instrukcjami uruchamiania Chrome z flagami.

Firefox

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

Wykrywanie cech

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

Prześlij opinię

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

Jeśli znajdziesz błędy lub nieoczekiwane działanie w implementacji Chrome, zgłoś je. Wybierz komponenty Blink>SecurityFeature>SanitizerAPI i udostępnij szczegóły, aby ułatwić śledzenie problemu.

Prezentacja

Aby zobaczyć interfejs Sanitizer API w działaniu, skorzystaj z Sandbox Sanitizer API przygotowanego przez Mike’a Westa:

Pliki referencyjne


Zdjęcie autorstwa Towfiqu BarbhuiyaUnsplash.