
Библиотека Go для безопасной генерации YAML и shell-команд, использующая синтаксически-зависимые шаблоны для обнаружения и блокировки атак с внедрением кода с помощью аннотаций для доверенных данных.
Это неофициальный продукт 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/templateto 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 }}
yamltemplateНазначение yamltemplate — гарантировать, что по умолчанию ни одна строка
во входных данных не повлияет на структуру результирующего YAML (только на значения).
Например, приведённый ниже шаблон будет совместим с yamltemplate как есть,
автоматически предотвращая любые инъекции из входных данных Name:
name: {{.Name}}
Однако любые узлы шаблона, которые должны изменять результирующую структуру
YAML, например, вставка произвольной YAML-конфигурации, должны быть явно
аннотированы как StructuralData:
config: {{ (StructuralData .Config) }}
Ещё один случай, когда требуется аннотация StructuralData, — это необходимость
включить полную карту (map) в структуру YAML. Использование только
StructuralData может позволить инъекциям пройти через ключ, поэтому здесь
нужен дополнительный уровень проверки:
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 с 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))
yamltemplateYAML с дублирующимися ключами. Дублирующиеся ключи — это нестандартный YAML, и они не поддерживаются этой библиотекой. Пожалуйста, отрефакторите ваш YAML-шаблон, чтобы удалить дублирующиеся ключи. Например:
- project:
members: member-a
members: member-b
На:
- project:
members: member-b
shtemplateshtemplate предназначен для генерации shell-скриптов с гарантией того,
что ни одна строка входных данных не сможет внедрить новые команды или флаги
без явной аннотации.
Например, шаблонный скрипт, предназначенный для вывода одной строки, не сможет
отобразиться, если эта строка внедряет новую команду `./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 }}).