
مكتبة 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/template إلى 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 هي عندما تحتاج إلى تضمين خريطة كاملة في بنية 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))
yamltemplateYAML بمفاتيح مكررة. المفاتيح المكررة هي 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:
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، لكن واجهة البرمجة هي نفسها فيما عدا ذلك. وshsprintf.MustSprintf متاح للحالات التي يكون فيها الذعر (panic) مقبولًا.
يأتي 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))
ls {{ range .Paths }}{{.}} {{end}}