bigywater.tech
Go · 112

html/template:上下文感知的转义

text/template 对 HTML 一无所知,把用户输入原样塞进页面就是 XSS。html/template 按 {{.}} 所在的位置(HTML 文本、属性、URL、JS、CSS)选择转义方式。这篇把同一个恶意输入放进不同位置,逐个看它被改成了什么,以及哪些写法会让保护失效。

#ZgotmplZ
href 里 javascript: 协议的值被替换成的占位符
\u003c
放进 script 块的字符串里,< 被编码成这个,避免提前结束 script 块
template.HTML
传这个类型等于告诉模板“这段已经安全”,会原样输出

位置决定转义方式

实测 · 本机执行输出
  [HTML 文本] html/template: <p>&lt;script&gt;alert(1)&lt;/script&gt;</p>
  [带引号的属性值] html/template: <a title="&#34; onmouseover=&#34;alert(1)">x</a>
  [不带引号的属性值] html/template: <a title=x&#32;onmouseover&#61;alert(1)>x</a>
  [href 里的 URL(危险协议)] html/template: <a href="#ZgotmplZ">x</a>
  [href 里的 URL(正常)] html/template: <a href="https://example.com/a%20b?x=1&amp;y=2">x</a>
  [URL 查询参数] html/template: <a href="/s?q=a%26b%3d%3cc%3e">x</a>
  [JavaScript 里的字符串] html/template: <script>var s = "\u003c/script\u003e\u003cscript\u003ealert(1)";</script>
  [JavaScript 里的数字] html/template: <script>var n =  42 ;</script>
  [JavaScript 里的对象] html/template: <script>var o = {"a":1,"b":"\u003cx\u003e"};</script>
  [CSS 属性值] html/template: <p style="color: ZgotmplZ">x</p>
  [HTML 注释里] html/template: x
  对比:text/template 处理 HTML 文本: <p><script>alert(1)</script></p>

同一个 {{.}},在 HTML 文本里是 &lt; 实体,在属性值里是 &#34;,在 URL 里是百分号编码,在 JS 里是字符串字面量或 JSON,在 CSS 里不安全的值直接变成 ZgotmplZ。text/template 则全部原样输出。

绕过

实测 · 本机执行输出
  传 string:<p>&lt;b&gt;粗体&lt;/b&gt;</p>
  传 template.HTML(声明“这段已经安全”):<p><b>粗体</b></p>
  传 template.HTML 但里面有脚本:<p><script>alert(1)</script></p>(调用方负责保证它安全)
  传 template.URL("javascript:alert(1)") 到 href:<a href="javascript:alert%281%29">x</a>

传 template.HTML 就会原样输出——哪怕里面是 <script>;传 template.URL 会跳过协议检查。这些类型是让你放入已经由你自己净化过的内容,不是让你把用户输入强转成它。

歧义时拒绝

实测 · 本机执行输出
  分支让上下文不一致:  ← 执行出错:html/template:t:1:5: {{if}} branches end in different contexts: {stateURL delimDoubleQuote urlPartNone jsCtxRegexp [] at…
  在 <script> 里拼 JS 标识符:<script>var "x" = 1;</script>
  JS 反引号模板字符串里的 {{.}}(值是 a`b):<script>var s = `a\u0060b`;</script>

{{if}} 的两个分支把模板带到不同的上下文,Execute 不会猜,而是报错并且不输出任何内容。把 {{.}} 当 JS 标识符用也不行,值只会变成字符串。

时机

实测 · 本机执行输出
  已经 Execute 过的模板再 Parse:html/template: cannot Parse after Execute

转义分析是在第一次 Execute 时完成的,之后再 Parse 会返回 cannot Parse after Execute。

文档

实测 · 本机执行输出
  This package wraps text/template so you can share its template API to parse and execute HTML templates safely.  tmpl, err := template.New("name").Parse(...) // Error checking elided err = tmpl.Execute(out, data)  If successful, tmpl will now be injection-safe.

文档的承诺是:Parse 和 Execute 都成功后,模板就是防注入的——但这个承诺不包括上面的绕过类型。

分步演示

html/template
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。每个例子是一个很小的模板加一个恶意输入;输出直接来自 html/template 的 Execute;错误信息超过 120 字符的部分被截断;读过的文档:go doc html/template 开头的一段
    • 没有验证:浏览器是否真的会执行 javascript:alert%281%29、内容安全策略(CSP)与模板转义的配合、模板里用 {{template}} 嵌套时的上下文传播、自定义函数返回值的转义、