Bezpieczne manipulacje DOM za pomocą interfejsu Sanitizer API

Nowy interfejs Sanitizer API ma na celu stworzenie niezawodnego procesora do bezpiecznego wstawiania dowolnych ciągów znaków na stronie.

Jack J
Jack J

Aplikacje cały czas korzystają z niezaufanych ciągów znaków, ale bezpieczne wyrenderowanie treści jako części dokumentu HTML może być wyzwaniem. Bez odpowiedniej staranności łatwo jest przypadkowo utworzyć skrypty między witrynami (XSS), które mogą zostać wykorzystane przez hakerów.

Aby zmniejszyć to ryzyko, nowa oferta pakietowa Sanitizer API ma na celu stworzenie niezawodnego procesora umożliwiającego bezpieczne wstawianie dowolnych ciągów znaków na stronie. W tym artykule przedstawiamy 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 do modelu DOM danych wejściowych użytkownika, ciągów zapytań, zawartości plików cookie itd. trzeba odpowiednio zmieniać znaczenie ciągów znaków. Szczególną uwagę należy zwrócić na manipulację DOM za pomocą metody .innerHTML, gdzie ciągi znaków bez zmiany znaczenia są typowym źródłem błędów XSS.

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

Jeśli zmienisz znaczenie znaków specjalnych HTML w ciągu wejściowym powyżej lub rozszerzysz go za pomocą funkcji .textContent, polecenie alert(0) nie zostanie wykonane. Nie można jednak użyć tej metody, by zachować w kodzie HTML dekoracje tekstowe, ponieważ identyfikator <em> dodany przez użytkownika jest również rozwinięty jako ciąg znaków.

Najlepiej nie uciekać, tylko dezynfekować.

Usuwam dane wejściowe użytkownika

Różnica między ucieczeniem a dezynfekcją

Zmiana znaczenia oznacza zastąpienie specjalnych znaków HTML encjami HTML.

Sanityzacja oznacza usuwanie szkodliwych semantycznie części (np. wykonania skryptu) z ciągów HTML.

Przykład

W poprzednim przykładzie reguła <img onerror> powoduje wykonanie modułu obsługi błędów, ale jeśli moduł onerror został usunięty, można go bezpiecznie rozwinąć 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 prawidłowo przeprowadzić proces oczyszczania, trzeba przetworzyć ciąg znaków wejściowych na format HTML, pominąć tagi i atrybuty, które są uznawane za szkodliwe, i zachować te nieszkodliwe.

Proponowana specyfikacja interfejsu Sanitizer API ma zapewnić takie przetwarzanie jak standardowy interfejs API dla przeglądarek.

Interfejs API Sanitizer

Interfejs Sanitizer API jest używany w taki 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>

Domyślnym argumentem jest jednak { sanitizer: new Sanitizer() }. Wygląda to tak jak poniżej.

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

Warto zauważyć, że definicja pola setHTML() to Element. Ponieważ jest to metoda Element, kontekst do analizy nie wymaga wyjaśnienia (w tym przypadku <div>). Analiza przeprowadza się raz wewnętrznie, a wynik jest bezpośrednio rozwijany do DOM.

Aby uzyskać wynik oczyszczania w postaci ciągu znaków, możesz użyć parametru .innerHTML z wyników 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 tak, aby usuwać ciągi znaków, które uruchamiałyby 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 dezynfekcji powinien traktować określony element.

allowElements: nazwy elementów, które powinny zachować środki dezynfekcyjne.

blockElements: nazwy elementów, które powinny zostać usunięte, ale zachowają dzieci.

dropElements: nazwy elementów, które mają zostać usunięte, wraz z nazwami elementów podrzędnych.

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 lub je odrzucać:

  • allowAttributes
  • dropAttributes

Właściwości allowAttributes i dropAttributes wymagają list dopasowania atrybutów – obiektów, których klucze są nazwami atrybutów, a wartości to listy elementów docelowych lub symbolu wieloznacznego *.

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 z funkcją dezynfekcji. Główną różnicą między interfejsami Sanitizer API a DOMPurify jest to, że DOMPurify zwraca wynik oczyszczania w postaci ciągu znaków, który trzeba 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 działać jako wartość zastępczą, jeśli interfejs Sanitizer API nie jest zaimplementowany w przeglądarce.

Implementacja DOMPurify ma kilka wad. Jeśli zostanie zwrócony ciąg znaków, będzie on analizowany dwukrotnie: przez DOMPurify i .innerHTML. Taka podwójna analiza marnuje czas przetwarzania, ale może też prowadzić do powstawania interesujących luk w zabezpieczeniach, które wynikają z sytuacji, gdy wynik drugiej analizy różni się od pierwszego.

Do analizy kodu HTML potrzebny jest też kontekst. Na przykład <td> ma sens w <table>, ale nie w <div>. Ponieważ DOMPurify.sanitize() przyjmuje tylko ciąg znaków jako argument, trzeba było odgadnąć kontekst analizy.

Interfejs Sanitizer API stanowi ulepszenie metody DOMPurify i ma na celu wyeliminowanie konieczności podwójnego analizowania i doprecyzowania kontekstu analizy.

Stan interfejsu API i obsługa przeglądarek

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

Krok Stan
1. Utwórz wyjaśnienie Zakończono
2. Utwórz wersję roboczą specyfikacji Zakończono
3. Zbieraj opinie i ulepszaj projekt Zakończono
4. Testowanie origin Chrome Zakończono
5. Uruchom Zamiar wysyłki M105

Mozilla: Uważa, że oferta warto utworzyć prototyp i aktywnie ją wdraża.

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

Jak włączyć interfejs Sanitizer API

Obsługa przeglądarek

  • Chrome: funkcja nieobsługiwana.
  • Edge: funkcja nie jest obsługiwana.
  • Firefox: za flagą.
  • Safari: nieobsługiwane.

Źródło

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

Chrome

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

Firefox

W przeglądarce Firefox jako funkcję eksperymentalną zastosowano także interfejs Sanitizer API. Aby go włączyć, ustaw flagę dom.security.sanitizer.enabled na true w about:config.

Wykrywanie cech

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

Prześlij opinię

Jeśli wypróbujesz ten interfejs API i chcesz podzielić się opinią, chętnie je poznamy. Podziel się swoimi przemyśleniami na temat problemów z GitHub API dotyczącym interfejsu Sanitizer i omów je z autorami specyfikacji oraz innymi osobami zainteresowanymi tym interfejsem API.

Jeśli znajdziesz błędy lub nieoczekiwane zachowanie implementacji Chrome, zgłoś błąd, aby go zgłosić. Wybierz komponenty Blink>SecurityFeature>SanitizerAPI i udostępnij szczegóły, aby pomóc we wdrażaniu i śledzeniu problemu.

Prezentacja

Aby zobaczyć, jak działa Sanitizer API, zajrzyj do Sanitizer API Playground autorstwa Mike'a Westa:

Pliki referencyjne


Zdjęcie: Towfiqu barbhuiya, Unsplash.