bluemonday 是一个用 Go 实现的 HTML 净化器。它速度快且高度可配置。
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 核心团队在 Go net/html 库 中实现的快速、非验证、仅向前、基于标记的解析器应用。
我们期望得到格式良好的 HTML(每个适用的开放元素都有对应的闭合元素,且嵌套正确),因此我们并不专注于修复嵌套错误或不完整的 HTML。我们只专注于确保存在的任何元素都在策略允许列表中有所描述,并确保属性和链接可安全用于你的网页。GIGO 确实适用,如果你向它输入糟糕的 HTML,bluemonday 的任务并不是想办法让它重新变好。
可以
我们正在生产环境中使用 bluemonday,之前使用的是广泛使用且经过大量现场测试的 OWASP Java HTML Sanitizer。
我们通过了完整的测试套件(包括 AntiSamy 测试以及针对任何已提出问题的测试)。请检查是否有任何未解决的问题,以确定是否有任何问题可能会阻碍你使用。
我们欢迎拉取请求和 issue,以帮助我们确保针对通过用户生成内容进行的各种攻击提供全面的防护。
使用 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、style、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-`))
或者通过添加属性来添加元素:
// 注意推荐模式,参见下面关于使用 .Matching() 的建议
p.AllowAttrs("nowrap").OnElements("td", "th")
同样,这也支持正则表达式模式匹配的替代方案:
p.AllowAttrs("nowrap").OnElementsMatching(regex.MustCompile(`^my-element-`))
属性可以添加到所有元素上:
p.AllowAttrs("dir").Matching(regexp.MustCompile("(?i)rtl|ltr")).Globally()
或者属性可以添加到特定元素上:
// 不是推荐模式,参见下面关于使用 .Matching() 的建议
p.AllowAttrs("value").OnElements("li")
始终建议让属性匹配一个模式。否则,HTML 属性中的 XSS 非常容易发生:
// \p{L} 匹配 Unicode 字母,\p{N} 匹配 Unicode 数字
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")
虽然可以使用带 Matching 规则的 AllowAttrs 来处理内联 CSS,但编写一个单一的巨型正则表达式来安全地处理你希望允许的所有内联 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 是一个允许列表工具……你需要明确告诉我们要允许什么),并且当禁用时,它将阻止所有本地和 scheme 相对 URL(即 href="localpage.html"、href="../home.html",甚至 href="//www.google.com" 都被视为相对 URL):
p.AllowRelativeURLs(true)
如果你已启用可解析 URL,则可以允许允许的 scheme(当提到 http 和 https 时通常称为协议)。请记住,允许上述选项中的相对 URL 将允许空 scheme:
p.AllowURLSchemes("mailto", "http", "https")
无论你是否已启用可解析 URL,你都可以强制所有 URL 带上 rel="nofollow" 属性。如果该属性不存在,它将被添加,但仅当 href 有效时才会添加:
// 这适用于带有 "href" 属性的 "a" "area" "link" 元素
p.RequireNoFollowOnLinks(true)
类似地,你可以强制所有 URL 在其 rel 属性中包含 "noreferrer"。
// 这适用于带有 "href" 属性的 "a" "area" "link" 元素
p.RequireNoReferrerOnLinks(true)
我们提供了一个便捷方法,可应用以上所有内容,但你仍需允许可链接元素,URL 规则才会被应用:
p.AllowStandardURLs()
p.AllowAttrs("cite").OnElements("blockquote", "q")
p.AllowAttrs("href").OnElements("a", "area")
p.AllowAttrs("src").OnElements("img")
关于链接的另一个复杂性是 RFC2397 中定义的数据 URI。数据 URI 允许使用以下格式内联提供图片:
<img src="data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=">
我们提供了一个辅助方法,用于验证数据 URI 链接的 mimetype 及随后的 base64 内容:
p.AllowDataURIImages()
该辅助方法将启用 GIF、JPEG、PNG 和 WEBP 图片。
需要指出的是,使用数据 URI 链接存在潜在的安全 风险。只有在已经信任内容的情况下,才应启用数据 URI 链接。
我们还有一些功能可以帮助处理用户生成的内容:
p.AddTargetBlankToFullyQualifiedLinks(true)
这将确保完全限定的(href 目标包含主机名的)锚点 <a href="" /> 链接会获得添加的 target="_blank"。
此外,在策略应用后,任何带有 target="_blank" 的链接,其 rel 属性也会被调整以添加 noopener。这意味着链接可能以 <a href="//host/path"/> 开始,最终变成 <a href="//host/path" rel="noopener" target="_blank">。重要的是,添加 noopener 是一项安全功能,而不是问题。浏览器有一个不幸的特性:通过 target="_blank" 打开的浏览器窗口仍然可以控制 opener(即你的网页),而这可以防止这种情况。相关背景可以在这里找到: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 是否结构有效,这可能会使这些元素绕过 WhatWG HTML 解析器标准 中内置的一些安全机制。
修复你糟糕的 HTML 不是 bluemonday 的职责,bluemonday 的职责仅仅是阻止恶意 HTML 通过。如果你有错配的 HTML 元素,或不符合规范的嵌套元素,这些都将保留。但如果你有结构良好的 HTML,bluemonday 不会破坏它。
bluemonday.UGCPolicy()),它涵盖了开发者所需的大约 90%,但做了他们不需要的更多事情,并移除他们不需要的额外内容,使其 100% 符合他们的需求table 元素不能是 caption 的后代,colgroup、thead、tbody、tfoot 和 tr 是允许的,并且不允许出现字符数据)