Oczyść / przepisz kod HTML po stronie klienta

82

Muszę wyświetlić zasoby zewnętrzne ładowane za pośrednictwem żądań między domenami i upewnić się, że wyświetlam tylko „ bezpieczne ” treści.

Można by użyć Prototype's String # stripScripts do usunięcia bloków skryptów. Ale opiekunowie tacy jak onclicklub onerrornadal tam są.

Czy jest jakaś biblioteka, która może przynajmniej

  • usuwanie bloków skryptów,
  • zabić programy obsługi DOM,
  • usuń tagi z czarnej listy (np.: embedlub object).

Czy są więc jakieś linki i przykłady związane z JavaScriptem?

aemkei
źródło
12
Nie ufaj odpowiedziom, które mogą to zrobić za pomocą wyrażeń regularnych stackoverflow.com/questions/1732348/ ...
Mikko Ohtamaa
Jak to jest bezpieczne? Czy użytkownicy nie mogą edytować javascript strony?
Daniel mówi Przywróć Monikę
tak, to nie jest „bezpieczne”, chyba że po prostu próbujesz zapobiec błędom zaufanych użytkowników.
Scott,

Odpowiedzi:

112

Aktualizacja 2016: dostępny jest teraz pakiet Google Closure oparty na środku dezynfekującym Caja.

Ma bardziej przejrzyste API, został przepisany w celu uwzględnienia API dostępnych w nowoczesnych przeglądarkach i lepiej współdziała z Closure Compiler.


Bezwstydna wtyczka: zobacz caja / plugin / html-sanitizer.js, aby zapoznać się ze środkiem dezynfekującym HTML po stronie klienta, który został dokładnie sprawdzony.

Jest na białej liście, a nie na czarnej liście, ale białe listy można konfigurować zgodnie z CajaWhitelists


Jeśli chcesz usunąć wszystkie tagi, wykonaj następujące czynności:

var tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*';

var tagOrComment = new RegExp(
    '<(?:'
    // Comment body.
    + '!--(?:(?:-*[^->])*--+|-?)'
    // Special "raw text" elements whose content should be elided.
    + '|script\\b' + tagBody + '>[\\s\\S]*?</script\\s*'
    + '|style\\b' + tagBody + '>[\\s\\S]*?</style\\s*'
    // Regular name
    + '|/?[a-z]'
    + tagBody
    + ')>',
    'gi');
function removeTags(html) {
  var oldHtml;
  do {
    oldHtml = html;
    html = html.replace(tagOrComment, '');
  } while (html !== oldHtml);
  return html.replace(/</g, '&lt;');
}

Ludzie powiedzą ci, że możesz utworzyć element i przypisać go, innerHTMLa następnie pobrać innerTextlub textContent, a następnie uciec z tego bytów. Nie rób tego. Jest podatny na wstrzyknięcie XSS, ponieważ <img src=bogus onerror=alert(1337)>uruchomi onerrorprocedurę obsługi, nawet jeśli węzeł nigdy nie jest dołączony do DOM.

Mike Samuel
źródło
5
Świetnie, wygląda na to, że jest tu trochę dokumentacji: code.google.com/p/google-caja/wiki/JsHtmlSanitizer
tmcw
3
Kod środka dezynfekującego Caja HTML wygląda świetnie, ale wymaga trochę kodu kleju (sąsiedniego cssparser.js, ale co ważniejsze, html4obiektu). Dodatkowo zanieczyszcza globalną windowwłasność. Czy istnieje wersja dla sieci WWW tego kodu? Jeśli nie, czy widzisz lepszy sposób na wyprodukowanie i utrzymanie takiego, niż stworzenie dla niego osobnego projektu?
phihag
1
@phihag, zapytaj w google-caja-discuss, a mogą wskazać Ci pakiet. Uważam, że zanieczyszczenie obiektu okna jest związane z kompatybilnością wsteczną, więc każda nowa wersja pakietu może tego nie potrzebować.
Mike Samuel
1
Okazuje się, że jest już pakiet dla przeglądarek internetowych.
phihag
2
@phihag Ten pakiet jest przeznaczony dla nodejs, a nie przeglądarek.
Jeffery do
40

Środek dezynfekujący Google Caja HTML można przygotować „do pracy w sieci”, osadzając go w programie do obsługi sieci . Wszelkie zmienne globalne wprowadzone przez środek odkażający będą zawarte w pliku roboczym, a przetwarzanie będzie odbywać się w jego własnym wątku.

W przypadku przeglądarek, które nie obsługują aplikacji Web Workers, możemy użyć elementu iframe jako oddzielnego środowiska, w którym działa środek odkażający. Timothy Chien ma polyfill , który właśnie to robi, używając ramek iframe do symulacji pracowników sieci, więc ta część jest wykonywana za nas.

Projekt Caja ma stronę wiki na temat używania Caja jako samodzielnego środka odkażającego po stronie klienta :

  • Sprawdź źródło, a następnie skompiluj, uruchamiając ant
  • Uwzględnij html-sanitizer-minified.jslub html-css-sanitizer-minified.jsna swojej stronie
  • Połączenie html_sanitize(...)

Skrypt roboczy musi tylko postępować zgodnie z tymi instrukcjami:

importScripts('html-css-sanitizer-minified.js'); // or 'html-sanitizer-minified.js'

var urlTransformer, nameIdClassTransformer;

// customize if you need to filter URLs and/or ids/names/classes
urlTransformer = nameIdClassTransformer = function(s) { return s; };

// when we receive some HTML
self.onmessage = function(event) {
    // sanitize, then send the result back
    postMessage(html_sanitize(event.data, urlTransformer, nameIdClassTransformer));
};

(Potrzeba trochę więcej kodu, aby biblioteka simworker działała, ale nie jest to ważne w tej dyskusji.)

Demo: https://dl.dropbox.com/u/291406/html-sanitize/demo.html

Jeffery To
źródło
Świetna odpowiedź. Jeffrey, czy możesz wyjaśnić, dlaczego dezynfekcja musi być w ogóle wykonana przez pracownika sieci?
Austin Wang
Pracownicy sieciowi @AustinWang nie są absolutnie niezbędni, ale ponieważ oczyszczanie może być potencjalnie kosztowne obliczeniowo i nie wymaga interakcji użytkownika, dobrze nadaje się do tego zadania. (Wspomniałem również o umieszczeniu zmiennych globalnych w głównej odpowiedzi).
Jeffery To
Nie mogę znaleźć porządnej dokumentacji do tej biblioteki. Gdzie / jak mogę określić białą listę elementów i atrybutów?
AsGoodAsItGets
@AsGoodAsItGets Jak opisano w komentarzu w aktualnej wersji , nameIdClassTransformerjest wywoływana dla każdej nazwy HTML, identyfikatora elementu i listy klas; powrót nullspowoduje usunięcie atrybutu. Edytując pliki JSON w src / com / google / caja / lang / html , możesz również dostosować, które elementy i atrybuty mają być umieszczone na białej liście.
Jeffery do
@JefferyTo przepraszam, może jestem zbyt głupi, ale nie rozumiem. Pliki JSON, do których się odnosisz, nie są używane w powyższym przykładzie i demonstracji. Chcę korzystać z biblioteki w przeglądarce, więc obejrzałem Twoje demo. Czy możesz zmodyfikować nameIdClassTranformerpowyższą funkcję, np. Aby odrzucić wszystkie <script>tagi i zaakceptować <b>i <i>tagi?
AsGoodAsItGets
20

Nigdy nie ufaj klientowi. Jeśli piszesz aplikację serwerową, załóż, że klient zawsze będzie przesyłać niehigieniczne, złośliwe dane. Jest to praktyczna zasada, która uchroni Cię przed kłopotami. Jeśli możesz, radziłbym wykonać wszystkie sprawdzanie poprawności i czyszczenie w kodzie serwera, o którym wiesz (w rozsądnym stopniu) nie będzie się bawić. Być może mógłbyś użyć aplikacji internetowej na serwerze jako proxy dla kodu klienta, który pobiera od strony trzeciej i oczyszcza go przed wysłaniem go do samego klienta?

[edytuj] Przepraszam, źle zrozumiałem pytanie. Jednak trzymam się mojej rady. Twoi użytkownicy będą prawdopodobnie bezpieczniejsi, jeśli wyczyścisz serwer przed wysłaniem go do nich.

Sean Edwards
źródło
19
W rzeczywistości, wraz ze wzrostem popularności node.js, rozwiązanie javascript może być również rozwiązaniem po stronie serwera. Tak przynajmniej tu trafiłem. Mimo wszystko jest to doskonała rada, według której warto żyć.
Nicholas Flynt
15

Teraz, gdy wszystkie główne przeglądarki obsługują elementy iframe w piaskownicy, myślę , że istnieje znacznie prostszy sposób, który może być bezpieczny. Chciałbym, żeby ta odpowiedź została przejrzana przez osoby lepiej zaznajomione z tego rodzaju kwestiami bezpieczeństwa.

UWAGA: Ta metoda na pewno nie będzie działać w IE 9 i wcześniejszych. Zobacz tę tabelę dla wersji przeglądarek obsługujących piaskownicę. (Uwaga: w tabeli wydaje się, że nie działa w Operze Mini, ale właśnie spróbowałem i zadziałało).

Chodzi o to, aby utworzyć ukryty element iframe z wyłączoną obsługą JavaScript, wkleić do niego niezaufany kod HTML i pozwolić mu go przeanalizować. Następnie możesz przejść po drzewie DOM i skopiować tagi i atrybuty, które są uważane za bezpieczne.

Białe listy pokazane tutaj to tylko przykłady. To, co najlepiej umieścić na białej liście, zależy od aplikacji. Jeśli potrzebujesz bardziej wyrafinowanych zasad niż tylko białe listy tagów i atrybutów, można je uwzględnić tą metodą, ale nie w tym przykładowym kodzie.

var tagWhitelist_ = {
  'A': true,
  'B': true,
  'BODY': true,
  'BR': true,
  'DIV': true,
  'EM': true,
  'HR': true,
  'I': true,
  'IMG': true,
  'P': true,
  'SPAN': true,
  'STRONG': true
};

var attributeWhitelist_ = {
  'href': true,
  'src': true
};

function sanitizeHtml(input) {
  var iframe = document.createElement('iframe');
  if (iframe['sandbox'] === undefined) {
    alert('Your browser does not support sandboxed iframes. Please upgrade to a modern browser.');
    return '';
  }
  iframe['sandbox'] = 'allow-same-origin';
  iframe.style.display = 'none';
  document.body.appendChild(iframe); // necessary so the iframe contains a document
  iframe.contentDocument.body.innerHTML = input;

  function makeSanitizedCopy(node) {
    if (node.nodeType == Node.TEXT_NODE) {
      var newNode = node.cloneNode(true);
    } else if (node.nodeType == Node.ELEMENT_NODE && tagWhitelist_[node.tagName]) {
      newNode = iframe.contentDocument.createElement(node.tagName);
      for (var i = 0; i < node.attributes.length; i++) {
        var attr = node.attributes[i];
        if (attributeWhitelist_[attr.name]) {
          newNode.setAttribute(attr.name, attr.value);
        }
      }
      for (i = 0; i < node.childNodes.length; i++) {
        var subCopy = makeSanitizedCopy(node.childNodes[i]);
        newNode.appendChild(subCopy, false);
      }
    } else {
      newNode = document.createDocumentFragment();
    }
    return newNode;
  };

  var resultElement = makeSanitizedCopy(iframe.contentDocument.body);
  document.body.removeChild(iframe);
  return resultElement.innerHTML;
};

Możesz to wypróbować tutaj .

Zwróć uwagę, że w tym przykładzie nie zezwalam na atrybuty stylu i tagi. Gdybyś im na to pozwolił, prawdopodobnie chciałbyś przeanalizować CSS i upewnić się, że jest bezpieczny do twoich celów.

Przetestowałem to na kilku nowoczesnych przeglądarkach (Chrome 40, Firefox 36 Beta, IE 11, Chrome na Androida) i na jednej starej (IE 8), aby upewnić się, że wyskoczyło przed wykonaniem jakichkolwiek skryptów. Chciałbym wiedzieć, czy są jakieś przeglądarki, które mają z tym problem, lub jakieś skrajne przypadki, które przeoczę.

aldel
źródło
10
Ten wpis zasługuje na uwagę ekspertów, bo wydaje się być oczywistym i najprostszym rozwiązaniem. Czy to naprawdę bezpieczne?
pwray
Jak można programowo utworzyć ukrytą ramkę iframe „z wyłączoną obsługą JavaScript”? O ile wiem, jest to niemożliwe. W chwili, gdy to zrobisz iframe.contentDocument.body.innerHTML = input, wszelkie tagi skryptów zostaną wykonane.
AsGoodAsItGets
@AsGoodAsItGets - wyszukaj atrybut piaskownicy w elementach iframe.
aldel
1
@aldel Rzeczywiście, nie wiedziałem o tym. Dla nas to nadal nie jest możliwe z powodu braku wsparcia w IE9. Wydaje mi się, że twoje rozwiązanie może zadziałać, ale myślę, że powinieneś wyjaśnić w swojej odpowiedzi, że polegasz na sandboxatrybucie.
AsGoodAsItGets
Przykro mi, pomyślałem, że to jasne z mojego otwarcia „Teraz, gdy wszystkie główne przeglądarki obsługują elementy iframe w piaskownicy”. Dodam mniej subtelną nutę.
aldel
12

Nie możesz przewidzieć każdego możliwego dziwnego rodzaju zniekształconych znaczników, o które jakaś przeglądarka może się potknąć, aby uniknąć czarnej listy, więc nie czarnej listy. Istnieje wiele innych struktur, które być może trzeba będzie usunąć, niż tylko skrypt / osadzenie / obiekt i programy obsługi.

Zamiast tego spróbuj przeanalizować kod HTML na elementy i atrybuty w hierarchii, a następnie uruchom wszystkie nazwy elementów i atrybutów na białej liście możliwie jak najmniejszej. Sprawdź również atrybuty adresu URL, które przepuszczasz, na białej liście (pamiętaj, że istnieją bardziej niebezpieczne protokoły niż tylko javascript :).

Jeśli dane wejściowe są dobrze sformułowane w XHTML, pierwsza część powyższego jest znacznie łatwiejsza.

Jak zawsze w przypadku sanityzacji HTML, jeśli możesz znaleźć inny sposób, aby tego uniknąć, zrób to. Potencjalnych dziur jest wiele, wiele. Jeśli po tylu latach główne usługi poczty internetowej nadal znajdują exploity, dlaczego myślisz, że możesz zrobić to lepiej?

bobince
źródło
11

npmMamy rok 2016 i myślę, że wielu z nas używa teraz modułów w naszym kodzie. sanitize-htmlwydaje się być wiodącą opcją na npm, chociaż są inne .

Inne odpowiedzi na to pytanie zapewniają duży wkład w tworzenie własnych, ale jest to na tyle trudny problem, że dobrze przetestowane rozwiązania społecznościowe są prawdopodobnie najlepszą odpowiedzią.

Uruchom to w wierszu poleceń, aby zainstalować: npm install --save sanitize-html

ES5: var sanitizeHtml = require('sanitize-html'); // ... var sanitized = sanitizeHtml(htmlInput);

ES6: import sanitizeHtml from 'sanitize-html'; // ... let sanitized = sanitizeHtml(htmlInput);

ericsoco
źródło
10
2018 tutaj, to jest za ciężkie (pół megabajta zależności)
user1464581
2020 tutaj, sanitize-html jest dla Node i nadal nie ma dobrej opcji dla przeglądarek, o ile wiem
Mick
3

[Zastrzeżenie: jestem jednym z autorów]

Napisaliśmy w tym celu bibliotekę open source „tylko internetową” (tj. „Wymaga przeglądarki”), https://github.com/jitbit/HtmlSanitizer, która usuwa wszystko tags/attributes/stylesoprócz tych z „białej listy”.

Stosowanie:

var input = HtmlSanitizer.SanitizeHtml("<script> Alert('xss!'); </scr"+"ipt>");

PS działa znacznie szybciej niż „czysty JavaScript”, ponieważ używa przeglądarki do analizowania i manipulowania DOM. Jeśli interesuje Cię „czysty JS”, ​​wypróbuj https://github.com/punkave/sanitize-html (nie jest powiązany)

Alex
źródło
2

Zasugerowana powyżej biblioteka Google Caja była zbyt złożona, aby ją skonfigurować i uwzględnić w moim projekcie dla aplikacji internetowej (czyli działającej w przeglądarce). Zamiast tego uciekłem się do tego, ponieważ używamy już komponentu CKEditor, aby użyć jego wbudowanej funkcji dezynfekcji HTML i białej listy, która jest znacznie łatwiejsza do skonfigurowania. Możesz więc załadować instancję CKEditor do ukrytego elementu iframe i zrobić coś takiego:

CKEDITOR.instances['myCKEInstance'].dataProcessor.toHtml(myHTMLstring)

Oczywiście, jeśli nie używasz CKEditor w swoim projekcie, może to być trochę przesada, ponieważ sam komponent ma około pół megabajta (zminimalizowany), ale jeśli masz źródła, może możesz odizolować kod, wykonując białej listy ( CKEDITOR.htmlParser?) i znacznie ją skrócić.

http://docs.ckeditor.com/#!/api

http://docs.ckeditor.com/#!/api/CKEDITOR.htmlDataProcessor

Lepiej Być Nie Może
źródło
0

Polecam wycinanie z życia frameworków, które na dłuższą metę znacznie ułatwiłyby życie.

cloneNode: Klonowanie kopie węzłów wszystkich jego atrybutów i ich wartości, ale nie NIE kopiować nasłuchiwania zdarzeń .

https://developer.mozilla.org/en/DOM/Node.cloneNode

Poniższe nie są testowane, chociaż używam treewalkerów od jakiegoś czasu i są one jedną z najbardziej niedocenianych części JavaScript. Oto lista typów węzłów, które możesz przeszukać, zwykle używam SHOW_ELEMENT lub SHOW_TEXT .

http://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html#Traversal-NodeFilter

function xhtml_cleaner(id)
{
 var e = document.getElementById(id);
 var f = document.createDocumentFragment();
 f.appendChild(e.cloneNode(true));

 var walker = document.createTreeWalker(f,NodeFilter.SHOW_ELEMENT,null,false);

 while (walker.nextNode())
 {
  var c = walker.currentNode;
  if (c.hasAttribute('contentEditable')) {c.removeAttribute('contentEditable');}
  if (c.hasAttribute('style')) {c.removeAttribute('style');}

  if (c.nodeName.toLowerCase()=='script') {element_del(c);}
 }

 alert(new XMLSerializer().serializeToString(f));
 return f;
}


function element_del(element_id)
{
 if (document.getElementById(element_id))
 {
  document.getElementById(element_id).parentNode.removeChild(document.getElementById(element_id));
 }
 else if (element_id)
 {
  element_id.parentNode.removeChild(element_id);
 }
 else
 {
  alert('Error: the object or element \'' + element_id + '\' was not found and therefore could not be deleted.');
 }
}
Jan
źródło
5
Ten kod zakłada, że ​​dane wejściowe do czyszczenia zostały już przeanalizowane, a nawet wstawione do drzewa dokumentu. W takim przypadku złośliwe skrypty zostały już wykonane. Wejście powinno być ciągiem.
phihag
Następnie wyślij do niego fragment DOM, tylko dlatego, że znajduje się on w DOM w danym kształcie lub formie, nie oznacza w rzeczywistości, że został wykonany. Zakładając, że ładuje go przez AJAX, może tego użyć w połączeniu z importNode.
John