Обеззараживать / переписывать HTML на стороне клиента
Мне нужно отображать внешние ресурсы, загруженные через междоменные запросы, и отображать только "безопасный" контент.
Может использовать Prototype's String # stripScripts для удаления блоков скриптов. Но обработчики, такие как onclick
или же onerror
все еще там.
Есть ли библиотека, которая может хотя бы
- блоки скриптовых блоков,
- убить обработчики DOM,
- удалить черные перечисленные теги (например:
embed
или жеobject
).
Так есть ли ссылки и примеры, связанные с JavaScript?
11 ответов
Обновление 2016: теперь существует пакет Google Closure, основанный на дезинфицирующем средстве Caja.
Он имеет более чистый API, был переписан с учетом API, доступных в современных браузерах, и лучше взаимодействует с Closure Compiler.
Бесстыдный плагин: см. Caja / plugin / html-sanitizer.js для html-дезинфицирующего средства на стороне клиента, который был тщательно рассмотрен.
Это белый список, не черный список, но белые списки настраиваются в соответствии с 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, '<');
}
Люди скажут вам, что вы можете создать элемент и назначить innerHTML
а затем получить innerText
или же textContent
и затем избегайте сущностей в этом. Не делай этого. Это уязвимо для инъекций XSS, так как <img src=bogus onerror=alert(1337)>
будет управлять onerror
обработчик, даже если узел никогда не подключен к DOM.
HTML-дезинфицирующее средство Google Caja можно сделать "готовым к работе в Интернете ", внедрив его в веб-приложение. Любые глобальные переменные, введенные дезинфицирующим средством, будут содержаться внутри рабочего, плюс обработка выполняется в собственном потоке.
Для браузеров, которые не поддерживают Web Workers, мы можем использовать iframe как отдельную среду для работы с дезинфицирующим средством. У Тимоти Чина есть полифилл, который делает именно это, используя iframes для имитации 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
Никогда не доверяй клиенту. Если вы пишете серверное приложение, предположите, что клиент всегда будет отправлять антисанитарные, вредоносные данные. Это эмпирическое правило, которое избавит вас от неприятностей. Если вы можете, я бы посоветовал сделать все проверки и санитарии в коде сервера, с которым вы знаете (в разумной степени) не будет возиться. Возможно, вы могли бы использовать серверное веб-приложение в качестве прокси для вашего клиентского кода, который выбирается у стороннего производителя и выполняет санитарную обработку перед отправкой его самому клиенту?
[править] Извините, я неправильно понял вопрос. Тем не менее, я придерживаюсь своего совета. Ваши пользователи, вероятно, будут в большей безопасности, если вы очистите сервер перед отправкой им.
Теперь, когда все основные браузеры поддерживают встроенные фреймы с песочницей, я думаю, что существует гораздо более простой способ обеспечения безопасности. Я был бы рад, если бы этот ответ был рассмотрен людьми, которые более знакомы с такой проблемой безопасности.
ПРИМЕЧАНИЕ. Этот метод определенно не будет работать в IE 9 и более ранних версиях. См. Эту таблицу для версий браузера, которые поддерживают песочницу.
Идея состоит в том, чтобы создать скрытый 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), чтобы убедиться, что он был запущен перед выполнением любых сценариев. Мне было бы интересно узнать, есть ли какие-либо браузеры, которые имеют проблемы с этим, или какие-либо крайние случаи, которые я пропускаю.
Итак, это 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);
Вы не можете предвидеть все возможные странные типы искаженной разметки, которые какой-то браузер может отключить, чтобы избежать внесения в черный список, поэтому не создавайте черный список Есть гораздо больше структур, которые вы, возможно, должны удалить, чем просто скрипт / встраивание / объект и обработчики.
Вместо этого попытайтесь разобрать HTML-код в элементы и атрибуты в иерархии, а затем запустите все имена элементов и атрибутов в минимально-возможном белом списке. Также проверьте любые атрибуты URL, которые вы пропускаете, в белый список (помните, что есть более опасные протоколы, чем просто javascript:).
Если вход XHTML правильно сформирован, то первая часть выше будет намного проще.
Как всегда в случае санации HTML, если вы можете найти другой способ избежать этого, сделайте это вместо этого. Есть много, много потенциальных дыр. Если основные службы веб-почты все еще находят эксплойты по прошествии стольких лет, что заставляет вас думать, что вы можете добиться большего успеха?
String.prototype.sanitizeHTML=function (white,black) {
if (!white) white="b|i|p|br";//allowed tags
if (!black) black="script|object|embed";//complete remove tags
var e=new RegExp("(<("+black+")[^>]*>.*</\\2>|(?!<[/]?("+white+")(\\s[^<]*>|[/]>|>))<[^<>]*>|(?!<[^<>\\s]+)\\s[^</>]+(?=[/>]))", "gi");
return this.replace(e,"");
}
-черный список -> полное удаление тега и содержимого
-белый список -> сохранить теги
другие теги удаляются, но содержимое тегов сохраняется
все атрибуты тегов белого списка (оставшиеся) удалены
[Отказ от ответственности: я один из авторов]
Для этого мы написали библиотеку с открытым исходным кодом "только для веб" (то есть "требуется браузер"), 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 (не аффилированный)
Предложенная выше библиотека Google Caja была слишком сложной для настройки и включения в мой проект для веб-приложения (то есть для работы в браузере). Вместо этого я прибег к тому, что, поскольку мы уже используем компонент CKEditor, это использование его встроенной функции очистки и внесения в белый список HTML, которую гораздо проще настроить. Таким образом, вы можете загрузить экземпляр CKEditor в скрытый iframe и сделать что-то вроде:
CKEDITOR.instances['myCKEInstance'].dataProcessor.toHtml(myHTMLstring)
Теперь, конечно, если вы не используете CKEditor в своем проекте, это может быть немного излишним, так как сам компонент занимает около половины мегабайта (свернуто), но если у вас есть источники, возможно, вы можете изолировать код, выполняя белый список (CKEDITOR.htmlParser
?) и сделать его намного короче.
Вместо использования регулярных выражений я подумал о способе использования нативного материала DOM. Таким образом, вы можете преобразовать HTML в документ, получить этот HTML и легко получить все определенные элементы и элементы белого списка и атрибуты для удаления. При этом используется список атрибутов либо как массив простых строк атрибутов для разрешения, либо может использоваться регулярное выражение для проверки их значений и разрешения только для определенных тегов.
Я рекомендую вырезать рамки из вашей жизни, в долгосрочной перспективе вам будет намного проще.
cloneNode: клонирование узла копирует все его атрибуты и их значения, но НЕ копирует прослушиватели событий.
https://developer.mozilla.org/en/DOM/Node.cloneNode
Следующее не проверено, хотя я использую treewalker в течение некоторого времени, и они являются одной из наиболее недооцененных частей JavaScript. Вот список типов узлов, которые вы можете сканировать, обычно я использую SHOW_ELEMENT или SHOW_TEXT.
http://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html
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.');
}
}