这不是 Google 官方支持的产品。
这些库采用 safe-by-construction(安全构造)方法生成 YAML 等格式,用来替代像 text/template 和 sprintf 这样不感知语法、易受注入漏洞影响的库。
由于 text/template 不感知它生成格式的语法,因此它无法提供任何针对注入漏洞的防护。
请考虑下面这个使用 text/template 生成 YAML 的 produceConfig 函数:
package main
import (
"bytes"
"fmt"
"text/template"
)
func produceConfig(params any) (error, string) {
tmpl, _ := template.New("test").Parse("{ hello: {{ .addressee }} }")
var buf bytes.Buffer
err := tmpl.Execute(&buf, params)
if err != nil {
return err, ""
}
return nil, buf.String()
}
func main() {
goodReplacements := map[string]interface{}{
"addressee": "safe",
}
err, config := produceConfig(goodReplacements)
if err == nil {
fmt.Println(config)
} else {
fmt.Printf("Error: %v\n", err)
}
badReplacements := map[string]interface{}{
"addressee": "world, oops: true",
}
err, config = produceConfig(badReplacements)
if err == nil {
fmt.Println(config)
} else {
fmt.Printf("Error: %v\n", err)
}
}
该程序演示了恶意 addressee 输入如何在模板执行结果中注入新的 YAML 键。
使用 text/template 时,发生这种情况不会遇到任何错误,程序输出将是:
{ hello: safe }
{ hello: world, oops: true }
如果从 text/template 换成 safetext/yamltemplate,这种注入就会被阻止,输出将变为:
{ hello: safe }
Error: YAML Injection Detected
text/template 替换说明在访问输入数据字段时,会自动应用注入检测。
也可以在任何函数调用的结果上手动启用:
{{ RetrieveUntrustedData | ApplyInjectionDetection }}
可以通过应用 StructuralData 注解,在特定字段上禁用注入逻辑:
{{ (StructuralData .x) }}
在将输入传递给某个函数且该输入不应被修改时(例如执行某种查找),也需要使用 StructuralData 注解:
name: {{ readFile (StructuralData .pathToName) | ApplyInjectionDetection }}
建议充分利用 text/template 的功能(如条件表达式、range 循环等),在可能的情况下避免使用 StructuralData 注解。例如,不要这样写:
properties:
{{ (StructuralData .PropertiesYaml) }}
请考虑这样写:
properties:{{ range .Properties }}
- {{ . }}{{ end }}
yamltemplateyamltemplate 的意图是确保默认情况下,输入数据中的任何字符串都不会影响最终 YAML 的结构(只影响值)。
例如,下面的模板可以直接与 yamltemplate 兼容,同时自动防止来自 Name 输入的任何注入:
name: {{.Name}}
然而,任何预期会改变最终 YAML 结构的模板节点(例如插入任意 YAML 配置)都需要显式地标注为 StructuralData:
config: {{ (StructuralData .Config) }}
另一种需要 StructuralData 注解的情况是,你需要将完整的 map 包含到 YAML 结构中。单独使用 StructuralData 可能会让注入通过键(key)穿过,因此这里需要额外的验证层:
labels:
{{- range $key, $value := .Labels }}
{{ (StructuralData $key | MapKey) }}: {{ $value }}
{{- end }}
对应的 Go 代码可能如下所示:
func mapKeyFunc(data any) (string, error) {
if v, ok := data.(string); ok {
matched, err := regexp.MatchString(`^[a-zA-Z0-9/\-.]+$`, v)
if err != nil {
return "", err
}
if !matched {
return "", fmt.Errorf("invalid characters in the key: %v", v)
}
return v, nil
}
return "", errors.New("invalid input")
} ...
tmp:= template.New("something")
tmp.Funcs(map[string]any{"MapKey":mapKeyFunc})
tmpl := template.Must(tmp.Parse(yamlTemplate))
yamltemplate 不支持的用例带重复键的 YAML。重复键是非标准 YAML,此库不支持。请重构你的 YAML 模板以移除重复键。例如:
- project:
members: member-a
members: member-b
改为:
- project:
members: member-b
shtemplateshtemplate 旨在让你生成 shell 脚本,并保证在无需显式注解的情况下,输入数据字符串都无法注入新的命令或标志(flag)。
例如,一个仅用于打印一个字符串的模板脚本,如果该字符串注入了新命令 `./evil`,则渲染将失败:
echo "{{ .addressee }}"
若要显式允许输入字符串包含并非来自模板字符串的新命令,可以使用 StructuralData 注解:
{{ (StructuralData .commands) }}
标志(以 - 开头的参数)在默认情况下也被禁止。例如,如果 Filename 是 --interactive,下面的模板将渲染失败:
git add {{ .Filename }}
若要显式允许作为命令参数传递的输入字符串是标志,可以使用 AllowFlags 注解:
git add {{ (AllowFlags .FilenameOrGitAddFlag) }}
默认情况下,从单个输入字符串生成多个参数也是被禁止的。这种构造应改用数组和 range 表达式来实现:
ls {{ range .Paths }}{{.}} {{end}}
text/template 替换不支持的用例在模板系统之外进行转义逻辑。相反,你应该将转义逻辑注解到模板中(例如:.UntrustedField | escape)。
部分格式。这些库设计用于生成完整文件。如果你生成片段然后再拼接,应该将此逻辑移到模板系统本身(使用 if 或 range 等构造)。
带有副作用的函数。这些库通过多次执行模板来工作,因此如果你注册有副作用的函数,可能会导致意外行为(例如:id: {{ AllocateID }})。
shsprintfshsprintf 旨在让你生成 shell 脚本,并保证无论潜在的转义是否正确,输入数据字符串都无法注入新的命令或标志。请参见下面的示例,该示例将返回错误 shsprintf.ErrShInjection,而不是带有注入命令的脚本:
message := "`whoami`"
result, err := shsprintf.Sprintf("git commit -m %s", message)
与 fmt.Sprintf 相比,shsprintf.Sprintf 增加了一个错误返回值,但 API 在其他方面是相同的。在可接受 panic 的情况下,可以使用 shsprintf.MustSprintf。
shsprintf 附带一个建议使用的转义函数:
message := "`whoami`"
result := shsprintf.MustSprintf("git commit -m %s", shsprintf.EscapeDefaultContext(message))
与 text/template 不同,这里没有特殊的注解。例如,如果需要传递多个参数,应通过修改格式字符串来实现:
files := []any{ "file1", "file2", "file3" }
result, err := shsprintf.Sprintf("cat" + strings.Repeat(" %s", len(files)), files...)
你可以将 yamltemplate 与 shprintf 结合使用。请考虑下面的 cloud-init YAML 模板:
---
write_files:
- path: /etc/nginx/refresh.sh
owner: root:root
permissions: 0755 # Don't forget the 0 (you are probably using octal...)
content: |
#!/bin/bash
set -euo pipefail
{{ shprintf `curl %s > /tmp/something` .userInput }}
使用 safetext/yamltemplate 渲染此模板时,shell 命令注入和 YAML 注入都将被阻止。
为此,你需要在 Go 端进行如下设置:
tmp:= addons.WithShsprintf(template.New("something"))
tmpl := template.Must(tmp.Parse(yamlTemplate))