Очистить / перезаписать HTML на стороне клиента

82

Мне нужно отображать внешние ресурсы, загруженные через междоменные запросы, и убедиться, что отображается только « безопасный » контент.

Можно использовать Prototype String # stripScripts для удаления блоков скрипта. Но такие обработчики как onclickили onerrorвсе еще существуют.

Есть ли какая-нибудь библиотека, которая может хотя бы

  • убрать блоки сценария,
  • убить обработчики DOM,
  • удалить теги из черного списка (например: embedили object).

Так есть ли какие-нибудь ссылки и примеры по JavaScript?

Aemkei
источник
12
Не верьте ответам, которые могут сделать это с помощью регулярных выражений stackoverflow.com/questions/1732348/…
Микко Охтамаа
Насколько это безопасно? Пользователи не могут редактировать javascript страницы?
Дэниел говорит: "Восстановите Монику"
да, это небезопасно, если вы просто не пытаетесь предотвратить ошибки доверенных пользователей.
Скотт

Ответы:

112

Обновление 2016: теперь существует пакет Google Closure на основе дезинфицирующего средства Caja.

Он имеет более чистый API, был переписан с учетом API, доступных в современных браузерах, и лучше взаимодействует с Closure Compiler.


Бесстыдный плагин: см. Caja / plugin / html-sanitizer.js, чтобы узнать о дезинфицирующем средстве на стороне клиента, которое было тщательно проверено.

Он внесен в белый список, а не в черный, но белые списки настраиваются в соответствии с CajaWhitelists


Если вы хотите удалить все теги, сделайте следующее:

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;');
}

Люди скажут вам, что вы можете создать элемент и назначить, innerHTMLа затем получить innerTextили textContent, а затем избежать сущностей в нем. Не делай этого. Он уязвим для внедрения XSS, поскольку <img src=bogus onerror=alert(1337)>будет запускать onerrorобработчик, даже если узел никогда не присоединен к DOM.

Майк Сэмюэл
источник
5
Отлично, похоже, здесь есть небольшая документация: code.google.com/p/google-caja/wiki/JsHtmlSanitizer
tmcw
3
Код дезинфицирующего средства Caja HTML выглядит великолепно, но требует некоторого связующего кода (соседнего cssparser.js, но, что более важно, html4объекта). Кроме того, это загрязняет глобальную windowсобственность. Есть ли версия этого кода для Интернета? Если нет, видите ли вы лучший способ создать и поддерживать его, чем создавать для него отдельный проект?
phihag
1
@phihag, Спросите в google-caja- Discusse, и они могут указать вам на упакованный. Я считаю, что загрязнение оконных объектов предназначено для обратной совместимости, поэтому любой новой версии пакета это может не понадобиться.
Майк Сэмюэл
1
Оказывается, уже есть пакет для веб-браузеров.
phihag
2
@phihag Этот пакет предназначен для nodejs, а не для браузеров.
Джеффри К
40

Дезинфицирующее средство Google Caja HTML можно сделать "готовым к работе в сети ", встроив его в веб-воркер . Любые глобальные переменные, введенные дезинфицирующим средством, будут содержаться внутри рабочего, плюс обработка происходит в его собственном потоке.

Для браузеров, которые не поддерживают Web Workers, мы можем использовать iframe как отдельную среду для работы sanitizer. Timothy Chien имеет полифил, который делает именно это, используя iframe для имитации Web Workers, так что эта часть сделана за нас.

У проекта Caja есть вики-страница о том, как использовать Caja в качестве автономного средства очистки на стороне клиента :

  • Проверьте исходный код, затем выполните сборку, запустив ant
  • Включите html-sanitizer-minified.jsили html-css-sanitizer-minified.jsна свою страницу
  • Вызов html_sanitize(...)

Рабочий скрипт должен только следовать этим инструкциям:

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));
};

(Для работы библиотеки simworker требуется немного больше кода, но это не важно для данного обсуждения.)

Демо: https://dl.dropbox.com/u/291406/html-sanitize/demo.html

Джеффри То
источник
Отличный ответ. Джеффри, не могли бы вы объяснить, почему санитарная обработка должна выполняться веб-воркером?
Остин Ван
@AustinWang Web-воркеры не являются строго обязательными, но, поскольку очистка потенциально может быть дорогостоящей в вычислительном отношении и не требует взаимодействия с пользователем, она хорошо подходит для этой задачи. (Я также упомянул, что в основном ответе есть глобальные переменные.)
Джеффри К
Я не могу найти достойную документацию для этой библиотеки. Где / как мне указать белый список элементов и атрибутов?
AsGoodAsItGets
@AsGoodAsItGets Как описано в комментарии в текущей версии , nameIdClassTransformerвызывается для каждого имени HTML, идентификатора элемента и списка классов; возврат nullудалит атрибут. Редактируя файлы JSON в src / com / google / caja / lang / html, вы также можете настроить, какие элементы и атрибуты будут добавлены в белый список.
Джеффри Кому
@JefferyTo извините, может я слишком тупой, но я не понимаю. Файлы JSON, на которые вы ссылаетесь, не используются в приведенном выше примере и демонстрации. Я хочу использовать библиотеку в браузере, поэтому посмотрел вашу демонстрацию. Можете ли вы изменить nameIdClassTranformerуказанную выше функцию, например, чтобы отклонить все <script>теги и принять <b>и <i>теги?
AsGoodAsItGets
20

Никогда не доверяйте клиенту. Если вы пишете серверное приложение, предполагайте, что клиент всегда будет отправлять антисанитарные вредоносные данные. Это практическое правило, которое убережет вас от неприятностей. Если можете, я бы посоветовал провести всю проверку и санацию в коде сервера, который, как вы знаете (в разумной степени), не будет возиться. Возможно, вы могли бы использовать серверное веб-приложение в качестве прокси для вашего клиентского кода, который извлекается от третьей стороны и выполняет очистку перед отправкой самому клиенту?

[править] Простите, я неправильно понял вопрос. Однако я верю своему совету. Ваши пользователи, вероятно, будут в большей безопасности, если вы продезинфицируете сервер перед его отправкой им.

Шон Эдвардс
источник
19
На самом деле, с ростом популярности node.js решение javascript также может быть серверным решением. По крайней мере, так я здесь оказался. Тем не менее, это отличный совет, по которому нужно жить.
Николас Флинт
15

Теперь, когда все основные браузеры поддерживают изолированные фреймы iframe, есть гораздо более простой способ, который, на мой взгляд, может быть безопасным. Я был бы рад, если бы этот ответ могли просмотреть люди, более знакомые с такого рода проблемами безопасности.

ПРИМЕЧАНИЕ. Этот метод определенно не будет работать в IE 9 и более ранних версиях. В этой таблице указаны версии браузеров, поддерживающих песочницу. (Примечание: таблица, похоже, говорит, что это не будет работать в Opera Mini, но я просто попробовал, и это сработало.)

Идея состоит в том, чтобы создать скрытый iframe с отключенным JavaScript, вставить в него ненадежный HTML-код и позволить ему проанализировать его. Затем вы можете пройтись по дереву DOM и скопировать теги и атрибуты, которые считаются безопасными.

Показанные здесь белые списки являются лишь примерами. Что лучше всего добавить в белый список, зависит от приложения. Если вам нужна более сложная политика, чем просто белые списки тегов и атрибутов, это можно сделать с помощью этого метода, но не с помощью этого примера кода.

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;
};

Вы можете попробовать это здесь .

Обратите внимание, что в этом примере я запрещаю атрибуты стиля и теги. Если вы позволите им, вы, вероятно, захотите проанализировать CSS и убедиться, что он безопасен для ваших целей.

Я тестировал это в нескольких современных браузерах (Chrome 40, Firefox 36 Beta, IE 11, Chrome для Android) и в одном старом (IE 8), чтобы убедиться, что он вышел из строя перед выполнением любых скриптов. Мне было бы интересно узнать, есть ли какие-либо браузеры, у которых есть проблемы с этим, или какие-либо крайние случаи, которые я упускаю.

Aldel
источник
10
Этот пост заслуживает внимания экспертов, так как кажется очевидным и простым решением. Это действительно безопасно?
pwray
Как можно программно создать скрытый iframe «с отключенным JavaScript»? Насколько мне известно, это невозможно. В ту минуту, когда вы это сделаете iframe.contentDocument.body.innerHTML = input, все теги сценария будут выполнены.
AsGoodAsItGets
@AsGoodAsItGets - найдите атрибут песочницы в окнах iframe.
aldel
1
@aldel Действительно, я не знал об этом. Для нас это все еще непозволительно из-за отсутствия поддержки в IE9. Думаю, ваше решение может сработать, но я думаю, вам следует пояснить в своем ответе, что вы зависите от sandboxатрибута.
AsGoodAsItGets
Извините, я подумал, что это ясно из моего открытия «Теперь, когда все основные браузеры поддерживают изолированные фреймы в песочнице». Я добавлю менее тонкое замечание.
aldel
12

Вы не можете предвидеть все возможные странные типы искаженной разметки, которые какой-то браузер где-то может споткнуться, чтобы избежать черного списка, поэтому не заносите в черный список. Есть много больше структур Вы , возможно , потребуется удалить , чем просто скрипт / вставлять / объекта и обработчиков.

Вместо этого попытайтесь разобрать HTML на элементы и атрибуты в иерархии, а затем запустить все имена элементов и атрибутов в минимально возможном белом списке. Также проверьте все пропущенные атрибуты URL-адресов в белом списке (помните, что существуют более опасные протоколы, чем просто javascript :).

Если ввод - это правильно сформированный XHTML, первая часть вышеизложенного намного проще.

Как всегда с дезинфекцией HTML, если вы можете найти другой способ избежать этого, сделайте это. Есть очень много потенциальных ям. Если основные службы веб-почты по прошествии многих лет все еще находят уязвимости, почему вы думаете, что можете добиться большего?

бобинс
источник
11

Итак, это 2016 год, и я думаю, что многие из нас сейчас используют npmмодули в нашем коде. sanitize-htmlкажется ведущим вариантом в npm, хотя есть и другие .

Другие ответы на этот вопрос дают большой вклад в то, как свернуть свой собственный, но это достаточно сложная проблема, и хорошо протестированные решения сообщества, вероятно, являются лучшим ответом.

Запустите это в командной строке для установки: 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);

Ericoco
источник
10
2018 здесь, это слишком тяжело (пол мегабайта зависимостей)
user1464581
2020 здесь, sanitize-html предназначен для Node, и, насколько я могу судить, до сих пор нет хорошего варианта для браузеров,
Мик,
3

[Отказ от ответственности: я один из авторов]

Для этого мы написали библиотеку с открытым исходным кодом «только для Интернета» (т.е. «требуется браузер») https://github.com/jitbit/HtmlSanitizer, которая удаляет все, tags/attributes/stylesкроме «белых».

Применение:

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

PS Работает намного быстрее, чем решение «на чистом JavaScript», поскольку оно использует браузер для анализа и управления DOM. Если вас интересует "чистое JS" решение, попробуйте https://github.com/punkave/sanitize-html (не аффилированный)

Alex
источник
2

Предложенная выше библиотека Google Caja была слишком сложной для настройки и включения в мой проект веб-приложения (то есть, работающего в браузере). Вместо этого, поскольку мы уже используем компонент CKEditor, я прибег к использованию встроенной функции очистки HTML и внесения в белый список, которую гораздо проще настроить. Итак, вы можете загрузить экземпляр CKEditor в скрытый iframe и сделать что-то вроде:

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

Конечно, если вы не используете CKEditor в своем проекте, это может быть немного излишним, поскольку размер самого компонента составляет около половины мегабайта (минимизирован), но если у вас есть исходники, возможно, вы можете изолировать код, выполняющий белый список ( CKEDITOR.htmlParser?) и сделать его намного короче.

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

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

Лучше не бывает
источник
0

Я рекомендую исключить рамки из вашей жизни, это значительно упростит вам жизнь в долгосрочной перспективе.

cloneNode: при клонировании узла копируются все его атрибуты и их значения, но это НЕ копировать слушатель событий .

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

Следующее не тестировалось, хотя я уже некоторое время использую Treewalkers, и они являются одной из самых недооцененных частей JavaScript. Вот список типов узлов, которые вы можете сканировать, обычно я использую SHOW_ELEMENT или 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.');
 }
}
Джон
источник
5
Этот код предполагает, что входные данные для очистки уже проанализированы и даже вставлены в дерево документа. В этом случае вредоносные сценарии уже выполнены. Ввод должен быть строкой.
phihag 04
Затем отправьте ему фрагмент DOM, только потому, что он находится в DOM в заданной форме или форме, на самом деле не означает, что он был выполнен. Предполагая, что он загружает его через AJAX, он может использовать это вместе с importNode.
Джон