
bluemonday: быстрый HTML-санитайзер на golang (вдохновлённый OWASP Java HTML Sanitizer) для очистки пользовательского контента от XSS
bluemonday — это HTML-санитайзер, реализованный на Go. Он быстрый и легко настраивается.
bluemonday принимает недоверенный пользовательский контент в качестве входных данных и возвращает HTML, очищенный по белому списку разрешённых HTML-элементов и атрибутов, чтобы вы могли безопасно включать такой контент на свою веб-страницу.
Если вы принимаете пользовательский контент, а ваш сервер написан на Go, вам нужен bluemonday.
Политика по умолчанию для пользовательского контента (bluemonday.UGCPolicy().Sanitize()) превращает это:
Hello <STYLE>.XSS{background-image:url("javascript:alert('XSS')");}</STYLE><A CLASS=XSS></A>World
в безвредное:
Hello World
А это:
<a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a>
превращает в это:
XSS
При этом продолжает разрешать вот это:
<a href="http://www.google.com/">
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/>
</a>
Пропускает почти без изменений (к нему добавился атрибут rel="nofollow", что хорошо для пользовательского контента):
<a href="http://www.google.com/" rel="nofollow">
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/>
</a>
Он защищает сайты от атак XSS. Существует множество векторов XSS-атак, и лучший способ снизить риск — очищать пользовательский ввод по известному безопасному списку HTML-элементов и атрибутов.
bluemonday следует всегда запускать после любой другой обработки.
Если вы используете blackfriday или Pandoc, то bluemonday следует запускать после этих шагов. Это гарантирует, что незащищённый HTML не появится позже в вашем процессе.
bluemonday во многом вдохновлён как OWASP Java HTML Sanitizer, так и HTML Purifier.
Основан на белом списке: вам нужно либо построить политику, описывающую HTML-элементы и атрибуты для разрешения (и regexp-шаблоны атрибутов), либо использовать одну из предоставленных политик с хорошими настройками по умолчанию.
Политика, содержащая белый список, применяется с помощью быстрого невалидирующего, однопроходного токенизирующего парсера, реализованного в библиотеке Go net/html основной командой Go.
Мы ожидаем корректно оформленный HTML (закрывающие элементы для каждого открытого элемента, правильная вложенность) и поэтому не занимаемся исправлением плохо вложенного или неполного HTML. Мы сосредоточены на том, чтобы все существующие элементы были описаны в белом списке политики, а атрибуты и ссылки были безопасны для использования на вашей веб-странице. Здесь действует принцип GIGO: если вы скормите ему плохой HTML, bluemonday не обязан придумывать, как сделать его снова хорошим.
Да
Мы используем bluemonday в продакшене, перейдя с широко используемого и многократно проверенного в полевых условиях OWASP Java HTML Sanitizer.
Мы проходим наш обширный набор тестов (включая тесты AntiSamy, а также тесты на любые поднятые задачи). Проверьте нерешённые задачи, чтобы понять, не может ли что-то стать для вас препятствием.
Мы приглашаем pull request'ы и issues, чтобы помочь нам гарантировать всестороннюю защиту от различных атак через пользовательский контент.
Установите с помощью go get github.com/microcosm-cc/bluemonday
Затем вызовите его:
package main
import (
"fmt"
"github.com/microcosm-cc/bluemonday"
)
func main() {
// Do this once for each unique policy, and use the policy for the life of the program
// Policy creation/editing is not safe to use in multiple goroutines
p := bluemonday.UGCPolicy()
// The policy can then be used to sanitize lots of input and it is safe to use the policy in multiple goroutines
html := p.Sanitize(
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`,
)
// Output:
// <a href="http://www.google.com" rel="nofollow">Google</a>
fmt.Println(html)
}
Мы предлагаем три способа вызова Sanitize:
p.Sanitize(string) string
p.SanitizeBytes([]byte) []byte
p.SanitizeReader(io.Reader) bytes.Buffer
Если вы одержимы производительностью, p.SanitizeReader(r).Bytes() вернёт []byte без выполнения лишних преобразований входов или выходов. Хотя разница настолько незначительна, что вам никогда не придётся об этом беспокоиться.
Вы можете создавать собственные политики:
package main
import (
"fmt"
"github.com/microcosm-cc/bluemonday"
)
func main() {
p := bluemonday.NewPolicy()
// Require URLs to be parseable by net/url.Parse and either:
// mailto: http:// or https://
p.AllowStandardURLs()
// We only allow <p> and <a href="">
p.AllowAttrs("href").OnElements("a")
p.AllowElements("p")
html := p.Sanitize(
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`,
)
// Output:
// <a href="http://www.google.com">Google</a>
fmt.Println(html)
}
Мы поставляем две политики по умолчанию:
bluemonday.StrictPolicy() — её можно считать эквивалентом удаления всех HTML-элементов и их атрибутов, поскольку в её белом списке ничего нет. Пример сценария использования — заголовки записей в блоге, где HTML-теги не ожидаются вовсе, а если они есть, то и элементы, и их содержимое должны быть удалены. Это очень строгая политика.bluemonday.UGCPolicy() — допускает широкий набор HTML-элементов и атрибутов, безопасных для пользовательского контента. Обратите внимание: эта политика не разрешает iframe, object, embed, стили, script и т.д. Пример сценария использования — тело записи в блоге, где ожидается разнообразное форматирование, а также возможны TABLE и IMG.Суть построения политики — определить, какие HTML-элементы и атрибуты считаются безопасными для вашего сценария. OWASP предоставляет шпаргалку по предотвращению XSS для объяснения рисков, но по сути:
script, style, iframe, object, embed, base, которые позволяют клиенту выполнять код или включать сторонний контент, способный выполнять кодПо сути, вы должны быть в состоянии описать, какой HTML допустим для вашего сценария. Если вы не уверены, что можете описать свою политику, подумайте об использовании одной из поставляемых политик, например bluemonday.UGCPolicy().
Чтобы создать новую политику:
p := bluemonday.NewPolicy()
Чтобы добавить элементы в политику, добавьте только элементы:
p.AllowElements("b", "strong")
Или используйте регулярное выражение:
Примечание: если элемент добавляется по имени, как показано выше, любое подходящее регулярное выражение будет проигнорировано
Также рекомендуется следить, чтобы несколько шаблонов не перекрывались, поскольку порядок выполнения не гарантируется и это может привести к пропуску некоторых правил.
p.AllowElementsMatching(regex.MustCompile(`^my-element-`))
Или добавляйте элементы путём добавления атрибута:
// Note the recommended pattern, see the recommendation on using .Matching() below
p.AllowAttrs("nowrap").OnElements("td", "th")
Опять же, здесь также поддерживается альтернативный вариант сопоставления по регулярному выражению:
p.AllowAttrs("nowrap").OnElementsMatching(regex.MustCompile(`^my-element-`))
Атрибуты можно добавлять либо ко всем элементам:
p.AllowAttrs("dir").Matching(regexp.MustCompile("(?i)rtl|ltr")).Globally()
Либо атрибуты можно добавлять к конкретным элементам:
// Not the recommended pattern, see the recommendation on using .Matching() below
p.AllowAttrs("value").OnElements("li")
Всегда рекомендуется задавать атрибуту сопоставление с шаблоном. Иначе XSS в HTML-атрибутах очень прост:
// \p{L} matches unicode letters, \p{N} matches unicode numbers
p.AllowAttrs("title").Matching(regexp.MustCompile(`[\p{L}\p{N}\s\-_',:\[\]!\./\\\(\)&]*`)).Globally()
Вы можете остановиться в любой момент и вызвать .Sanitize():
// string htmlIn passed in from a HTTP POST
htmlOut := p.Sanitize(htmlIn)
Вы также можете взять любую существующую политику и расширить её:
p := bluemonday.UGCPolicy()
p.AllowElements("fieldset", "select", "option")
Хотя обрабатывать встроенный CSS можно с помощью AllowAttrs с правилом Matching, написать единое монолитное регулярное выражение для безопасной обработки всего встроенного CSS, который вы хотите разрешить, — нетривиальная задача. Вместо того чтобы пытаться это сделать, вы можете разрешить атрибут style на любых нужных элементах и использовать стилевые политики для контроля и очистки встроенных стилей.
Настоятельно рекомендуется использовать Matching (с подходящим регулярным выражением), MatchingEnum или MatchingHandler, чтобы каждый стиль соответствовал вашим потребностям, но для большинства широко используемых стилей предусмотрены обработчики по умолчанию.
Как и в случае с атрибутами, вы можете разрешить задавать конкретные CSS-свойства во встроенных стилях:
p.AllowAttrs("style").OnElements("span", "p")
// Allow the 'color' property with valid RGB(A) hex values only (on any element allowed a 'style' attribute)
p.AllowStyles("color").Matching(regexp.MustCompile("(?i)^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$")).Globally()
Кроме того, вы можете разрешить устанавливать CSS-свойству только определённое допустимое значение:
p.AllowAttrs("style").OnElements("span", "p")
// Allow the 'text-decoration' property to be set to 'underline', 'line-through' or 'none'
// on 'span' elements only
p.AllowStyles("text-decoration").MatchingEnum("underline", "line-through", "none").OnElements("span")
Или вы можете задавать элементы на основе сопоставления с регулярным выражением:
p.AllowAttrs("style").OnElementsMatching(regex.MustCompile(`^my-element-`))
// Allow the 'text-decoration' property to be set to 'underline', 'line-through' or 'none'
// on 'span' elements only
p.AllowStyles("text-decoration").MatchingEnum("underline", "line-through", "none").OnElementsMatching(regex.MustCompile(`^my-element-`))
Если вам нужна более точная проверка, вы можете создать обработчик, который принимает строку и возвращает bool для проверки значений конкретного свойства. Строковый параметр уже преобразован в нижний регистр, а кодовые точки Unicode преобразованы.
myHandler := func(value string) bool{
// Validate your input here
return true
}
p.AllowAttrs("style").OnElements("span", "p")
// Allow the 'color' property with values validated by the handler (on any element allowed a 'style' attribute)
p.AllowStyles("color").MatchingHandler(myHandler).Globally()
Ссылки — это сложные объекты для безопасной очистки, а также один из крупнейших векторов атак вредоносного контента.
Можно сделать так:
p.AllowAttrs("href").Matching(regexp.MustCompile(`(?i)mailto|https?`)).OnElements("a")
Но это вас не защитит, так как в данном случае регулярного выражения недостаточно, чтобы предотвратить непредвиденные действия некорректного значения.
Мы предоставляем несколько дополнительных глобальных опций для безопасной работы со ссылками.
RequireParseableURLs гарантирует, что URL-адреса разбираются пакетом Go net/url:
p.RequireParseableURLs(true)
Если вы включили разбираемые URL, то следующая опция будет AllowRelativeURLs. По умолчанию она отключена (bluemonday — это инструмент белых списков... вам нужно явно сказать нам, что разрешить), и при отключении она блокирует все локальные и схема-относительные URL (например, href="localpage.html", href="../home.html" и даже href="//www.google.com" являются относительными):
p.AllowRelativeURLs(true)
Если вы включили разбираемые URL, вы можете разрешить допустимые схемы (обычно называемые протоколами, когда речь идёт об http и https). Имейте в виду, что разрешение относительных URL в предыдущей опции допускает пустую схему:
p.AllowURLSchemes("mailto", "http", "https")
Независимо от того, включили ли вы разбираемые URL, вы можете принудительно задать всем URL атрибут rel="nofollow". Он будет добавлен, если его нет, но только когда href корректен:
// This applies to "a" "area" "link" elements that have a "href" attribute
p.RequireNoFollowOnLinks(true)
Аналогично, вы можете принудительно указать noreferrer в атрибуте rel всех URL.
// This applies to "a" "area" "link" elements that have a "href" attribute
p.RequireNoReferrerOnLinks(true)
Мы предоставляем удобный метод, который применяет всё вышеперечисленное, но вам всё равно нужно разрешить связываемые элементы, к которым будут применяться правила URL:
p.AllowStandardURLs()
p.AllowAttrs("cite").OnElements("blockquote", "q")
p.AllowAttrs("href").OnElements("a", "area")
p.AllowAttrs("src").OnElements("img")
Дополнительная сложность, связанная со ссылками, — это data URI, определённый в RFC2397. Data URI позволяет встраивать изображения в таком формате:
<img src="data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=">
Мы предоставили вспомогательный метод для проверки mimetype и следующего за ним base64-содержимого ссылок data URI:
p.AllowDataURIImages()
Этот вспомогательный метод включит изображения GIF, JPEG, PNG и WEBP.
Следует отметить, что использование data URI ссылок несёт потенциальный риск для безопасности. Включайте data URI ссылки, только если вы уже доверяете содержимому.
У нас также есть несколько функций для работы с пользовательским контентом:
p.AddTargetBlankToFullyQualifiedLinks(true)
Это гарантирует, что полностью квалифицированные якорные ссылки <a href="" /> (адрес href содержит имя хоста) получат добавленный атрибут target="_blank".
Кроме того, любая ссылка, у которой после применения политики есть target="_blank", получит также скорректированный атрибут rel с добавлением noopener. Это означает, что ссылка может изначально выглядеть как <a href="//host/path"/> и в итоге стать <a href="//host/path" rel="noopener" target="_blank">. Важно отметить, что добавление noopener — это функция безопасности, а не проблема. К сожалению, браузеры устроены так, что окно, открытое с помощью target="_blank", всё ещё может управлять окном-открывателем (вашей веб-страницей), и эта функция защищает от этого. Подробнее об этом можно прочитать здесь: https://dev.to/ben/the-targetblank-vulnerability-by-example
Мы также включаем несколько вспомогательных функций для упрощения построения политик:
// Permits the "dir", "id", "lang", "title" attributes globally
p.AllowStandardAttributes()
// Permits the "img" element and its standard attributes
p.AllowImages()
// Permits ordered and unordered lists, and also definition lists
p.AllowLists()
// Permits HTML tables and all applicable elements and non-styling attributes
p.AllowTables()
Следующее недопустимо:
// This does not say where the attributes are allowed, you need to add
// .Globally() or .OnElements(...)
// This will be ignored without error.
p.AllowAttrs("value")
// This does not say where the attributes are allowed, you need to add
// .Globally() or .OnElements(...)
// This will be ignored without error.
p.AllowAttrs(
"type",
).Matching(
regexp.MustCompile("(?i)^(circle|disc|square|a|A|i|I|1)$"),
)
Оба примера демонстрируют одну и ту же проблему: они объявляют атрибуты, но затем не указывают, разрешены ли они глобально или только на конкретных элементах (и на каких). Атрибуты принадлежат одному или нескольким элементам, и политика должна объявлять это.
Мы пока не включаем инструменты для разрешения и очистки CSS. Это означает, что если вы не хотите делать всю тяжёлую работу в одном регулярном выражении (что не рекомендуется), вам не следует разрешать атрибут "style" где бы то ни было.
В той же теме оба элемента <script> и <style> считаются вредоносными. Эти элементы (и их содержимое) по умолчанию не отображаются, и для их разрешения требуется явно установить p.AllowUnsafe(true). Следует понимать, что разрешение этих элементов сводит на нет цель использования HTML-санитайзера, так как вы явно разрешаете либо JavaScript (и любой явно написанный XSS), либо CSS (который может изменить DOM для вставки JS). Кроме того, ограничения этой библиотеки означают, что она не знает, правильно ли структурирован HTML, и это может позволить этим элементам обойти некоторые механизмы безопасности, встроенные в стандарт парсера HTML WhatWG.
Задача bluemonday — не исправлять ваш плохой HTML, а лишь не пропускать вредоносный HTML. Если у вас есть непарные HTML-элементы или несоответствующая вложенность элементов, они останутся. Но если у вас корректно структурированный HTML, bluemonday его не сломает.
bluemonday.UGCPolicy()), которая покрывает 90% их потребностей, но делает больше, чем нужно, и удалить лишнее, чтобы сделать её на 100% соответствующей их желаниямtable не является потомком caption, разрешены colgroup, thead, tbody, tfoot и tr, а символьные данные не допускаются)