Go 语言标准库 html/template 包深入浅出

3,012 阅读3分钟

html/template 包是对 text/template 包的二次封装,增加了一些安全性的处理,核心的接口和逻辑都是一样的。

安全性

渲染模板技术一直存在跨站脚本攻击的风险,本质上是网站将用户的输入不作转义写入生成的页面中,如果用户提交一段浏览器脚本,则会在用户的页面中执行,进而产生不可预知的风险。

html/template 自动开启安全模式将需要编码的数据处理成纯文本,各种不同的转义上下文(escaping contextual)可以安全的嵌入 HTML 模板,如 JavaScript、CSS 和 URI 上下文。

举个例子:

import "text/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")

输出:

Hello, <script>alert('you have been pwned')</script>!

可以看到 text/template 包中的 JavaScript 上下文没有被转义。

import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")

输出:

Hello, <script>alert('you have been pwned')</script>!

JavaScript 上下文被成功转义。

上下文

包内有这几种上下文: HTML、CSS、JavaScript 和 URI,在不同的上下文中会自动添加不同的转义函数。

<a href="/search?q={{.}}">{{.}}</a>

在解析时,每一个 {{.}} 根据所处的上下文,都会被添加转义函数。

<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>

URL 上下文中的添加 urlescaper 和 attrescaper 转义函数。

HTML 上下文中的添加 htmlescaper 转义函数

假设 {{.}} 的字符串表示为 O'Reilly: How are <i>you</i>? 其中包含多个有害字符,下面是在不同上下文中的不同转义结果:

Context                          {{.}} After
{{.}}                            O'Reilly: How are &lt;i&gt;you&lt;/i&gt;?
<a title='{{.}}'>                O&#39;Reilly: How are you?
<a href="/{{.}}">                O&#39;Reilly: How are %3ci%3eyou%3c/i%3e?
<a href="?q={{.}}">              O&#39;Reilly%3a%20How%20are%3ci%3e...%3f
<a onx='f("{{.}}")'>             O\x27Reilly: How are \x3ci\x3eyou...?
<a onx='f({{.}})'>               "O\x27Reilly: How are \x3ci\x3eyou...?"
<a onx='pattern = /{{.}}/;'>     O\x27Reilly: How are \x3ci\x3eyou...\x3f

可以发现,'、<、>、? 在不同的上下文中会被转义成不同的字符编码。

如果 {{.}} 只包括无害字符,如字符串 left,则不会进行任何转义。

Context                              {{.}} After
{{.}}                                left
<a title='{{.}}'>                    left
<a href='{{.}}'>                     left
<a href='/{{.}}'>                    left
<a href='?dir={{.}}'>                left
<a style="border-{{.}}: 4px">        left
<a style="align: {{.}}">             left
<a style="background: '{{.}}'>       left
<a style="background: url('{{.}}')>  left
<style>p.{{.}} {color:red}</style>   left

没有任何字符串可以在 JavaScript 上下文中使用。

如果 {{.}} 等于结构体 struct{A, B string}{ "foo", "bar" } 在模板 <script>var pair = {{.}};</script> 会渲染成:

<script>var pair = {"A": "foo", "B": "bar"};</script>

也就是将结构体用 json 包 marshaled 序列化之后嵌入 JavaScript 上下文中。

避免转义的方法

默认情况下,html/template 包假设所有的流(pipeline)提供纯文本的输入。添加转义流阶段必须正确和安全的嵌入不同上下文的纯文本流中。

当一个数据不是纯文本时,需要确保不会对它进行转义。

类型 HTML,JS,URL 和其他来自 content.go 的类型可以避免转义,因为它们不是纯文本!

说白了就是传入的时候不传传文本,使用各种类型的函数包装一下再传递。

避免转义的方法:

// 模板
Hello, {{.}}!

// 使用 template.HTML 包装
tmpl.Execute(out, template.HTML(`<b>World</b>`))

输出:

// 没有被转义
Hello, <b>World</b>!


// 添加转义函数
{{ html .HTMLContent }}

类型函数有这么几种:CSS、HTML、HTMLAttr、JS、JSStr、URL 和 Srcset。

总结

因为是 HTML 模板,最终会生成前端页面,为了保证安全性,在内部将不同的字符串识别成不同的上下文,对每种上下文会自动添加不同的转义函数对不同的内容进行转义,如果不想内容被转义,可以把纯文本使用各种类型函数包装,包装过的纯文本会被转义函数忽略。

参考资料