
안전한 YAML 및 셸 생성을 위한 Go 라이브러리로, 구문 인식 템플릿을 사용하여 신뢰할 수 있는 데이터에 대한 어노테이션을 통해 주입 공격을 탐지하고 차단합니다.
이 제품은 공식적으로 지원되는 Google 제품이 아닙니다.
YAML과 같은 형식을 생성하기 위한 safe-by-construction 라이브러리입니다. text/template이나 sprintf처럼 생성하는 형식의 구문을 인식하지 못해 주입 취약점에 노출된 라이브러리를 대체합니다.
text/template은 자신이 생성하는 형식의 구문을 인식하지 못하므로 주입 취약점에 대한 보호 기능을 제공하지 않습니다.
YAML을 생성하기 위해 text/template을 사용하는 다음 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 어노테이션은 입력값이 변경되지 않아야 하는 함수에 입력을 전달할 때도 필요합니다. 예를 들어 일종의 조회(lookup)를 수행하는 경우입니다:
name: {{ readFile (StructuralData .pathToName) | ApplyInjectionDetection }}
가능하면 조건식, range 루프 등 text/template의 기능을 최대한 활용해 StructuralData 어노테이션을 피하는 것이 좋습니다. 예를 들어 다음 코드 대신:
properties:
{{ (StructuralData .PropertiesYaml) }}
다음과 같이 작성하세요:
properties:{{ range .Properties }}
- {{ . }}{{ end }}
yamltemplateyamltemplate의 목적은 기본적으로 입력 데이터의 어떤 문자열도 결과 YAML의 구조에 영향을 미치지 못하게 하고 값에만 영향을 미치도록 하는 것입니다.
예를 들어 아래 템플릿은 Name 입력으로 인한 모든 주입을 자동으로 방지하면서 yamltemplate과 그대로 호환됩니다:
name: {{.Name}}
그러나 결과 YAML 구조를 변경할 것으로 예상되는 템플릿 노드(예: 임의의 YAML 설정 삽입)는 명시적으로 StructuralData로 어노테이션해야 합니다:
config: {{ (StructuralData .Config) }}
StructuralData 어노테이션이 필요한 또 다른 경우는 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은 명시적인 어노테이션 없이도 입력 데이터 문자열 중 어느 것도 새 명령이나 플래그를 주입할 수 없도록 보장하면서 셸 스크립트를 생성할 수 있게 설계되었습니다.
예를 들어 문자열 하나만 출력하도록 설계된 템플릿 스크립트는 해당 문자열이 새 명령 `./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).
부분 형식(partial format). 이 라이브러리들은 완전한 파일을 생성하도록 설계되었습니다. 세그먼트를 생성한 다음 서로 연결한다면, 이 로직을 템플릿 시스템 자체로 옮겨야 합니다(if나 range 같은 구문 사용).
부작용이 있는 함수. 이 라이브러리들은 템플릿을 여러 번 실행하는 방식으로 작동하므로, 부작용이 있는 함수를 등록하면 예기치 않은 동작이 발생할 수 있습니다(예: id: {{ AllocateID }}).
shsprintfshsprintf는 잠재적으로 잘못된 이스케이프와 관계없이 입력 데이터 문자열 중 어느 것도 새 명령이나 플래그를 주입할 수 없도록 보장하면서 셸 스크립트를 생성할 수 있게 설계되었습니다. 아래 예제를 보면, 명령이 주입된 스크립트 대신 shsprintf.ErrShInjection 오류가 반환됩니다:
message := "`whoami`"
result, err := shsprintf.Sprintf("git commit -m %s", message)
shsprintf.Sprintf는 fmt.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으로 평가하면 셸 명령 및 YAML 주입이 모두 방지됩니다.
이렇게 하려면 golang 측을 다음과 같이 설정해야 합니다:
tmp:= addons.WithShsprintf(template.New("something"))
tmpl := template.Must(tmp.Parse(yamlTemplate))