Bezpieczne manipulacje DOM za pomocą interfejsu Sanitizer API

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

Jack J
Jack J

Aplikacje przez cały czas obsługują niezaufane ciągi, ale bezpieczne renderowanie ich jako części dokumentu HTML może być trudne. Bez odpowiedniej staranności łatwo jest przypadkowo utworzyć możliwości wykorzystania przez cyberprzestępców ataków typu cross-site scripting (XSS).

Aby ograniczyć to ryzyko, nowa oferta interfejsu Sanitizer API ma na celu zbudowanie niezawodnego procesora, który umożliwia bezpieczne wstawianie dowolnych ciągów znaków na stronie. W tym artykule opisujemy interfejs API i wyjaśniamy, jak z niego korzystać.

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

Zmiana znaczenia danych wejściowych użytkownika

Podczas wstawiania danych wejściowych użytkownika, ciągów zapytań, zawartości plików cookie itd. do obiektu DOM ciągi znaków muszą mieć odpowiednie znaczenie zmiany znaczenia. Szczególną uwagę należy zwrócić na manipulację DOM za pomocą .innerHTML, gdzie ciągi znaków bez zmiany znaczenia są typowym źródłem XSS.

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

Jeśli w powyższym ciągu wejściowym zmienisz znaczenie znaków specjalnych HTML lub rozwiniesz go za pomocą funkcji .textContent, polecenie alert(0) nie zostanie wykonane. Ponieważ jednak atrybut <em> dodany przez użytkownika jest też rozwijany jako ciąg znaków, nie można użyć tej metody, by zachować dekorację tekstu w kodzie HTML.

Najlepszą rzeczą, jaką należy zrobić, jest oczyszczenie, a nie ucieczkę.

Dane wejściowe użytkownika dotyczące dezynfekcji

Różnica między ucieczką a oczyszczaniem

Zmiana znaczenia oznacza zastępowanie znaków specjalnych HTML encjami HTML.

Sanityzacja polega na usuwaniu z ciągów HTML elementów szkodliwych dla semanty (np. wykonywania skryptów).

Przykład

W poprzednim przykładzie funkcja <img onerror> powoduje wykonanie modułu obsługi błędów, ale jeśli moduł onerror został usunięty, można bezpiecznie rozwinąć go w DOM, pozostawiając <em> bez zmian.

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

Aby zadbać o prawidłową higienę, należy przeanalizować ciąg wejściowy jako kod HTML, pominąć tagi i atrybuty, które są uznawane za szkodliwe, i pozostawić te, które są nieszkodliwe.

Proponowana specyfikacja interfejsu Sanitizer API ma zapewnić takie przetwarzanie w standardowym interfejsie API dla przeglądarek.

Interfejs API Sanitizer

Interfejs Sanitizer API jest używany w następujący 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 domyślnym argumentem jest { sanitizer: new Sanitizer() }. Może to więc wyglądać tak jak poniżej.

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

Warto zauważyć, że właściwość setHTML() jest definiowana w elemencie Element. Ponieważ jest to metoda Element, kontekst do przeanalizowania nie jest jasny (w tym przypadku <div>), analiza jest przeprowadzana raz wewnętrznie, a wynik jest bezpośrednio rozszerzany do modelu DOM.

Aby uzyskać wynik oczyszczania w postaci ciągu znaków, możesz użyć funkcji .innerHTML z wyników wyszukiwania z usługi setHTML().

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

Dostosuj za pomocą konfiguracji

Interfejs Sanitizer API jest domyślnie skonfigurowany w taki sposób, aby usuwać ciągi tekstowe, które aktywują wykonanie skryptu. Możesz też dodać własne modyfikacje do procesu oczyszczania 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 oczyszczania powinien traktować określony element.

allowElements: nazwy elementów, które środek do dezynfekcji powinien zachować.

blockElements: nazwy elementów, które sanitizer musi usunąć przy zachowaniu dzieci.

dropElements: nazwy elementów, które środek dezynfekcyjny powinien usunąć wraz z dziećmi.

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>

Za pomocą tych opcji możesz też określić, czy sanitizer będzie zezwalać na określone atrybuty czy je odrzucać:

  • allowAttributes
  • dropAttributes

Właściwości allowAttributes i dropAttributes oczekują list dopasowania atrybutów, czyli 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 zezwalania na elementy niestandardowe lub ich odrzucania. 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 dobrze znana biblioteka oferująca funkcję oczyszczania. Główna różnica między interfejsem Sanitizer API a DOMPurify polega na tym, że DOMPurify zwraca wynik procesu oczyszczania w postaci ciągu 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="">`

Funkcja DOMPurify może zostać użyta jako kreacja zastępcza, jeśli w przeglądarce nie zaimplementowano interfejsu Sanitizer API.

Implementacja DOMPurify ma kilka wad. Jeśli ciąg zostanie zwrócony, to ciąg wejściowy jest analizowany dwukrotnie przez DOMPurify i .innerHTML. Taka podwójna analiza skraca czas potrzebny na przetwarzanie danych, ale może też prowadzić do powstawania interesujących luk w zabezpieczeniach spowodowanych przypadkami, gdy wynik drugiej analizy różni się od pierwszego.

Analizowanie kodu HTML wymaga też podania kontekstu. Na przykład <td> ma sens w języku <table>, ale nie w <div>. Funkcja DOMPurify.sanitize() przyjmuje tylko ciąg znaków jako argument, więc kontekst analizy musi być odgadnięty.

Interfejs Sanitizer API stanowi ulepszenie w stosunku do metody DOMPurify, ponieważ został zaprojektowany tak, aby wyeliminować konieczność podwójnego analizowania i uściślić kontekst analizy.

Stan interfejsu API i obsługa przeglądarek

Interfejs Sanitizer API jest obecnie omawiany w procesie standaryzacji, a obecnie jest on wdrażany w Chrome.

Step Stan
1. Utwórz wyjaśnienie Zakończono
2. Utwórz wersję roboczą specyfikacji Zakończono
3. Zbieranie opinii i ulepszanie projektu Zakończono
4. Testowanie origin Chrome Zakończono
5. Wprowadzenie na rynek Zamiar wysyłki w wersji M105

Mozilla: uważa tę propozycję za warto utworzyć prototyp i aktywnie ją wdraża.

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

Jak włączyć interfejs Sanitizer API

Obsługa przeglądarek

  • x
  • x
  • x

Źródło

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

Chrome

Jesteśmy w trakcie wdrażania interfejsu Sanitizer API w Chrome. W Chrome 93 i nowszych wersjach możesz wypróbować tę funkcję, włączając flagę about://flags/#enable-experimental-web-platform-features. We wcześniejszych wersjach Chrome Canary i deweloperskiej możesz ją włączyć w aplikacji --enable-blink-features=SanitizerAPI i wypróbować ją już teraz. Zapoznaj się z instrukcjami uruchamiania Chrome z flagami.

Firefox

W przeglądarce Firefox funkcja Sanitizer API jest również funkcją eksperymentalną. Aby je włączyć, ustaw flagę dom.security.sanitizer.enabled na true w interfejsie about:config.

Wykrywanie funkcji

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

Prześlij opinię

Jeśli wypróbujesz ten interfejs API i chcesz przekazać nam swoją opinię, chętnie poznamy Twoją opinię. Podziel się swoimi przemyśleniami na temat problemów z Sanitizer API na GitHubie i omów je z autorami specyfikacji oraz osobami zainteresowanymi tym interfejsem API.

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

Pokaz

Aby zobaczyć, jak działa interfejs Sanitizer API, zajrzyj do narzędzia Sanitizer API Playground opracowanego przez Mike'a Westa:

Źródła


Autor zdjęcia: Towfiqu barbhuiya, Unsplash.