
Biblioteca Go para geração segura de YAML e shell, usando templates cientes de sintaxe para detectar e bloquear ataques de injeção por meio de anotações para dados confiáveis.
Este não é um produto oficialmente suportado pelo Google.
Bibliotecas seguras por construção para produzir formatos como YAML, para substituir
bibliotecas que não têm consciência de sintaxe, como text/template e sprintf, que estão em risco de
vulnerabilidades de injeção.
Como o text/template não tem consciência de sintaxe dos formatos que produz, ele não
oferece qualquer proteção contra vulnerabilidades de injeção.
Considere a seguinte função produceConfig, que usa text/template para
gerar 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)
}
}
Este programa demonstra como uma entrada addressee maliciosa pode causar a injeção
de novas chaves YAML no resultado da execução do template.
Com text/template, nenhum erro será encontrado quando isso acontecer, e a
saída do programa será:
{ hello: safe }
{ hello: world, oops: true }
Ao trocar de text/template para safetext/yamltemplate, a
injeção teria sido evitada, com a saída sendo, em vez disso:
{ hello: safe }
Error: YAML Injection Detected
text/templateA detecção de injeção é aplicada automaticamente ao acessar campos de dados de entrada.
Ela também pode ser ativada manualmente no resultado de qualquer chamada de função:
{{ RetrieveUntrustedData | ApplyInjectionDetection }}
A lógica de injeção pode ser desativada em certos campos aplicando a
anotação StructuralData:
{{ (StructuralData .x) }}
A anotação StructuralData também é necessária ao passar uma entrada para uma
função onde a entrada não deve ser alterada, como ao realizar algum tipo de
busca:
name: {{ readFile (StructuralData .pathToName) | ApplyInjectionDetection }}
É recomendado usar completamente os recursos do text/template, como
expressões condicionais, loops de intervalo, etc., para evitar a anotação
StructuralData quando possível. Por exemplo, em vez de:
properties:
{{ (StructuralData .PropertiesYaml) }}
Considere:
properties:{{ range .Properties }}
- {{ . }}{{ end }}
yamltemplateA intenção do yamltemplate é garantir que, por padrão, nenhuma das strings
nos dados de entrada afete a estrutura do YAML resultante (apenas os valores).
Por exemplo, o template abaixo seria compatível com o yamltemplate como está,
ao mesmo tempo que previne automaticamente qualquer injeção proveniente da entrada Name:
name: {{.Name}}
No entanto, qualquer nó do template que de fato deva alterar a estrutura do YAML
resultante, como inserir configuração YAML arbitrária, precisaria ser
anotado explicitamente como StructuralData:
config: {{ (StructuralData .Config) }}
Outro caso em que a anotação StructuralData é necessária seria quando você
precisa incluir um mapa completo na estrutura YAML. Usar
StructuralData sozinho pode permitir que injeções passem pela chave, então
precisamos de uma camada extra de validação aqui:
labels:
{{- range $key, $value := .Labels }}
{{ (StructuralData $key | MapKey) }}: {{ $value }}
{{- end }}
O lado correspondente em golang poderia ser assim:
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))
yamltemplateYAML com chaves duplicadas. Chaves duplicadas são YAML não padronizado e não são suportadas por esta biblioteca. Por favor, refatore seu template YAML para remover chaves duplicadas. Por exemplo:
- project:
members: member-a
members: member-b
Para:
- project:
members: member-b
shtemplateO shtemplate foi projetado para permitir que você gere scripts shell com a
garantia de que nenhuma das strings de dados de entrada será capaz de injetar novos
comandos ou flags, sem anotação explícita.
Por exemplo, um script de template projetado apenas para imprimir uma string falhará
ao ser renderizado se essa string injetar um novo comando `./evil`:
echo "{{ .addressee }}"
Para permitir explicitamente que uma string de entrada contenha novos comandos que não sejam da
string do template, a anotação StructuralData pode ser usada:
{{ (StructuralData .commands) }}
Flags (argumentos que começam com -) também são proibidas por padrão. Por
exemplo, o template abaixo falhará ao ser renderizado se Filename for
--interactive:
git add {{ .Filename }}
Para permitir explicitamente que uma string de entrada passada como argumento de comando seja uma
flag, a anotação AllowFlags pode ser usada:
git add {{ (AllowFlags .FilenameOrGitAddFlag) }}
Múltiplos argumentos a partir de uma única string de entrada também são proibidos por padrão.
Esta construção deve, em vez disso, ser implementada usando um array e uma expressão
range:
text/templateLógica de escape fora do sistema de templates. Em vez disso, você deve
anotar a lógica de escape dentro do seu template (EX: .UntrustedField | escape).
Formatos parciais. As bibliotecas foram projetadas para serem usadas na geração de
arquivos completos. Se você gera segmentos e depois os concatena,
você deve, em vez disso, mover essa lógica para dentro do próprio sistema de templates (usando
construções como if ou range).
Funções com efeitos colaterais. As bibliotecas funcionam realizando múltiplas
execuções de template, então se você registrar funções que tenham efeitos colaterais,
isso pode causar comportamento inesperado (EX: id: {{ AllocateID }}).
shsprintfO shsprintf foi projetado para permitir que você gere scripts shell com a
garantia de que nenhuma das strings de dados de entrada será capaz de injetar novos
comandos ou flags, independentemente de escapes potencialmente incorretos. Veja o exemplo
abaixo, que retornará o erro shsprintf.ErrShInjection em vez do
script com um comando injetado:
message := "`whoami`"
result, err := shsprintf.Sprintf("git commit -m %s", message)
shsprintf.Sprintf adiciona um valor de retorno de erro em comparação com fmt.Sprintf, mas
a API é a mesma caso contrário. shsprintf.MustSprintf está disponível para casos
em que panic é aceitável.
O shsprintf vem com uma função de escape cujo uso é recomendado:
message := "`whoami`"
result := shsprintf.MustSprintf("git commit -m %s", shsprintf.EscapeDefaultContext(message))
Diferentemente do text/template, não há anotações especiais. Se você precisar
passar múltiplos argumentos, por exemplo, isso deve ser feito alterando a string de
formato:
files := []any{ "file1", "file2", "file3" }
result, err := shsprintf.Sprintf("cat" + strings.Repeat(" %s", len(files)), files...)
Você pode combinar yamltemplate com shprintf. Considere o seguinte
template YAML de cloud-init:
---
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 }}
Ao avaliar este template com safetext/yamltemplate, tanto injeções de comandos shell quanto injeções YAML serão evitadas.
Para fazer isso, você precisa configurar o lado golang assim:
tmp:= addons.WithShsprintf(template.New("something"))
tmpl := template.Must(tmp.Parse(yamlTemplate))
ls {{ range .Paths }}{{.}} {{end}}