これはGoogleの公式サポート製品ではありません。
YAMLなどの形式を生成するための、安全性を構造的に保証するライブラリ群です。text/template や sprintf のように、生成する形式の構文を理解しないがためにインジェクション脆弱性のリスクがあるライブラリの置き換えを目的としています。
text/template は生成する形式の構文を認識しないため、インジェクション脆弱性に対する保護を一切提供しません。
以下の produceConfig 関数は、text/template を使ってYAMLを生成します。
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 }}
可能な限り、条件式や range ループなどの text/template 機能を最大限に活用して、StructuralData アノテーションを避けることを推奨します。例えば、次のような書き方ではなく:
properties:
{{ (StructuralData .PropertiesYaml) }}
次のように検討してください:
properties:{{ range .Properties }}
- {{ . }}{{ end }}
yamltemplateyamltemplate の意図は、デフォルトでは入力データ内のどの文字列も、結果のYAMLの構造(値だけではなく)に影響を与えないことを保証することです。
例えば、以下のテンプレートは yamltemplate とそのまま互換性があり、Name 入力からのインジェクションを自動的に防ぎます:
name: {{.Name}}
ただし、結果のYAML構造を変更することが 想定されている テンプレートノード(任意のYAML設定を挿入するなど)は、明示的に StructuralData として注釈する必要があります:
config: {{ (StructuralData .Config) }}
StructuralData アノテーションが必要になるもう1つのケースは、完全なマップをYAML構造に含める必要がある場合です。StructuralData だけを使用すると、キーを介してインジェクションが通過する可能性があるため、ここでは追加の検証レイヤーが必要です:
labels:
{{- range $key, $value := .Labels }}
{{ (StructuralData $key | MapKey) }}: {{ $value }}
{{- end }}
対応する golang 側は次のようになります:
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 は、明示的な注釈なしでは、入力データのどの文字列も新しいコマンドやフラグを注入できないことを保証して、シェルスクリプトを生成できるように設計されています。
例えば、単に1つの文字列を出力するように設計されたテンプレートスクリプトは、その文字列が新しいコマンド `./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 は、潜在的に誤ったエスケープに関係なく、入力データのどの文字列も新しいコマンドやフラグを注入できないことを保証して、シェルスクリプトを生成できるように設計されています。次の例を参照してください。これは、注入されたコマンドを含むスクリプトの代わりに、エラー shsprintf.ErrShInjection を返します:
message := "`whoami`"
result, err := shsprintf.Sprintf("git commit -m %s", message)
shsprintf.Sprintf は、fmt.Sprintf と比較してエラー戻り値が追加されていますが、それ以外のAPIは同じです。パニックが許容される場合には、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 で評価すると、シェルコマンドとYAMLの両方のインジェクションが防止されます。
これを行うには、golang 側を次のように設定する必要があります:
tmp:= addons.WithShsprintf(template.New("something"))
tmpl := template.Must(tmp.Parse(yamlTemplate))