
Biblioteca de Go para la generación segura de YAML y shell, que utiliza plantillas sensibles a la sintaxis para detectar y bloquear ataques de inyección mediante anotaciones para datos de confianza.
Este no es un producto con soporte oficial de Google.
Bibliotecas seguras por construcción para producir formatos como YAML, para reemplazar
bibliotecas que no conocen la sintaxis, como text/template y sprintf, que corren el
riesgo de sufrir vulnerabilidades de inyección.
Dado que text/template no conoce la sintaxis de los formatos que produce, no
ofrece ninguna protección contra las vulnerabilidades de inyección.
Considera la siguiente función produceConfig, que usa text/template para
generar 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 demuestra cómo una entrada maliciosa addressee puede provocar la
inyección de nuevas claves YAML en el resultado de la ejecución de la plantilla.
Con text/template, no se encontrará ningún error cuando esto suceda, y la
salida del programa será:
{ hello: safe }
{ hello: world, oops: true }
En cambio, al cambiar de text/template a safetext/yamltemplate, la
inyección se habría evitado, y la salida en su lugar sería:
{ hello: safe }
Error: YAML Injection Detected
text/templateLa detección de inyecciones se aplica automáticamente al acceder a los campos de datos de entrada.
También puede habilitarse manualmente en el resultado de cualquier llamada a función:
{{ RetrieveUntrustedData | ApplyInjectionDetection }}
La lógica de inyección puede deshabilitarse en ciertos campos aplicando la
anotación StructuralData:
{{ (StructuralData .x) }}
La anotación StructuralData también se necesita al pasar una entrada a una
función donde la entrada no debe mutarse, como al realizar algún tipo de
búsqueda:
name: {{ readFile (StructuralData .pathToName) | ApplyInjectionDetection }}
Se recomienda aprovechar al máximo las funciones de text/template, como
expresiones condicionales, bucles range, etc., para evitar la anotación
StructuralData siempre que sea posible. Por ejemplo, en lugar de:
properties:
{{ (StructuralData .PropertiesYaml) }}
Considera:
properties:{{ range .Properties }}
- {{ . }}{{ end }}
yamltemplateLa intención de yamltemplate es garantizar que, por defecto, ninguna de las
cadenas de los datos de entrada afecte la estructura del YAML resultante (solo
los valores).
Por ejemplo, la siguiente plantilla sería compatible con yamltemplate tal
cual, mientras previene automáticamente cualquier inyección desde la entrada
Name:
name: {{.Name}}
Sin embargo, cualquier nodo de plantilla que se espere que cambie la
estructura del YAML resultante, como insertar configuración YAML arbitraria,
necesitaría anotarse explícitamente como StructuralData:
config: {{ (StructuralData .Config) }}
Otro caso en el que se necesita la anotación StructuralData sería cuando
necesitas incluir un mapa completo en la estructura YAML. Usar
StructuralData por sí solo puede permitir que las inyecciones pasen a
través de la clave, por lo que aquí se necesita una capa adicional de
validación:
labels:
{{- range $key, $value := .Labels }}
{{ (StructuralData $key | MapKey) }}: {{ $value }}
{{- end }}
El lado correspondiente en golang podría verse así:
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 con claves duplicadas. Las claves duplicadas son YAML no estándar y no son compatibles con esta biblioteca. Por favor, refactoriza tu plantilla YAML para eliminar las claves duplicadas. Por ejemplo:
- project:
members: member-a
members: member-b
A:
- project:
members: member-b
shtemplateshtemplate está diseñado para permitirte generar scripts de shell con la
garantía de que ninguna de las cadenas de datos de entrada podrá inyectar nuevos
comandos o flags, sin anotación explícita.
Por ejemplo, un script de plantilla diseñado para imprimir solo una cadena
fallará al renderizarse si esa cadena inyecta un nuevo comando
`./evil`:
echo "{{ .addressee }}"
Para permitir explícitamente que una cadena de entrada contenga nuevos
comandos que no provienen de la cadena de la plantilla, se puede usar la
anotación StructuralData:
{{ (StructuralData .commands) }}
Los flags (argumentos que comienzan con -) también están prohibidos por
defecto. Por ejemplo, la siguiente plantilla fallará al renderizarse si
Filename es --interactive:
git add {{ .Filename }}
Para permitir explícitamente que una cadena de entrada pasada como argumento
de comando sea un flag, se puede usar la anotación AllowFlags:
git add {{ (AllowFlags .FilenameOrGitAddFlag) }}
Múltiples argumentos desde una sola cadena de entrada también están prohibidos por defecto. Esta construcción debería implementarse en su lugar usando un array y una expresión :
text/templateLógica de escape fuera del sistema de plantillas. En su lugar, debes anotar
la lógica de escape dentro de tu plantilla (p. ej.: .UntrustedField | escape).
Formatos parciales. Las bibliotecas están diseñadas para generar archivos
completos. Si generas segmentos y luego los concatenas, deberías mover esa
lógica al propio sistema de plantillas (usando construcciones como if o
range).
Funciones con efectos secundarios. Las bibliotecas funcionan realizando
múltiples ejecuciones de plantilla, por lo que si registras funciones que
tienen efectos secundarios, esto podría causar un comportamiento inesperado
(p. ej.: id: {{ AllocateID }}).
shsprintfshsprintf está diseñado para permitirte generar scripts de shell con la
garantía de que ninguna de las cadenas de datos de entrada podrá inyectar nuevos
comandos o flags, independientemente de un escape potencialmente incorrecto. Ve
el siguiente ejemplo, que devolverá el error shsprintf.ErrShInjection en lugar
del script con un comando inyectado:
message := "`whoami`"
result, err := shsprintf.Sprintf("git commit -m %s", message)
shsprintf.Sprintf añade un valor de retorno de error en comparación con
fmt.Sprintf, pero por lo demás la API es la misma. shsprintf.MustSprintf
está disponible para casos donde un panic es aceptable.
shsprintf incluye una función de escape cuyo uso se recomienda:
message := "`whoami`"
result := shsprintf.MustSprintf("git commit -m %s", shsprintf.EscapeDefaultContext(message))
A diferencia de text/template, no hay anotaciones especiales. Si necesitas
pasar múltiples argumentos, por ejemplo, esto debe hacerse modificando la cadena
de formato:
files := []any{ "file1", "file2", "file3" }
result, err := shsprintf.Sprintf("cat" + strings.Repeat(" %s", len(files)), files...)
Puedes combinar yamltemplate con shprintf. Considera la siguiente
plantilla 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 }}
Al evaluar esta plantilla con safetext/yamltemplate, se evitarán tanto las inyecciones de comandos shell como las de YAML.
Para ello, debes configurar el lado golang así:
tmp:= addons.WithShsprintf(template.New("something"))
tmpl := template.Must(tmp.Parse(yamlTemplate))
rangels {{ range .Paths }}{{.}} {{end}}