Files
ARTEX/notify/webhook_template_test.go
T
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

113 lines
4.9 KiB
Go

package notify
import (
"context"
"reflect"
"sort"
"strings"
"testing"
)
// This file locks down the **capability boundary** of the generic Webhook template.
//
// This is the only place in the whole package where "a user-provided string is
// evaluated as code", so we must spell out what it can and cannot do, and pin those
// properties down with tests — otherwise one day someone will casually add a method
// to the template context, or a readFile to the FuncMap, and the capability surface
// silently expands while the diff looks like a harmless little function.
// TestTemplateContextHasNoMethods is the most important one.
//
// text/template invokes exported methods ({{.Foo}} can read a field or call a
// method). So as long as the template context can reach **any** type with exported
// methods, those methods are effectively exposed to template authors.
// This feature's context is deliberately pure data (exported fields only, zero
// methods).
//
// If this test fails: someone added a method to webhookTemplateData / webhookItem.
// Before deciding to allow it, think carefully about whether that method could be
// used by a template to read things that should not be exposed.
func TestTemplateContextHasNoMethods(t *testing.T) {
for _, v := range []any{webhookTemplateData{}, webhookItem{}} {
typ := reflect.TypeOf(v)
if n := typ.NumMethod(); n != 0 {
var names []string
for i := 0; i < n; i++ {
names = append(names, typ.Method(i).Name)
}
t.Fatalf("%s exposes %d methods (%s): text/template can call them, "+
"which hands those methods' capabilities to template authors", typ.Name(), n, strings.Join(names, ", "))
}
}
}
// TestTemplateFuncsAreMinimal locks down the set of functions exposed to templates.
//
// Every extra function in the FuncMap is one more capability. Currently there are
// only json / jsons, whose job is to serialize a value into a JSON fragment — they
// cannot read files, make requests, or execute commands.
func TestTemplateFuncsAreMinimal(t *testing.T) {
var got []string
for name := range webhookTemplateFuncs {
got = append(got, name)
}
sort.Strings(got)
want := []string{"json", "jsons"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("template function set changed: got %v, want %v. Before adding a function, confirm it does not expand the capability surface "+
"(no file reads/writes, no network requests, no command execution)", got, want)
}
}
// TestTemplateCannotReachUnknownData covers out-of-bounds access in templates:
// accessing something that does not exist must fail, not echo something back;
// and the failure message must not carry internal data out.
func TestTemplateCannotReachUnknownData(t *testing.T) {
_, err := renderWebhookBody(`{"x": {{.Environment}}, "y": {{.Env}}}`, singleMsg())
if err == nil {
t.Fatal("accessing a non-existent field should error")
}
// The error must not contain real content from the template context (vulnerability title/summary).
for _, leak := range []string{"SQL injection", "param id"} {
if strings.Contains(err.Error(), leak) {
t.Errorf("template error leaked message content %q: %v", leak, err)
}
}
}
// TestTemplateRenderFailsPermanently: a broken template is a configuration error; retries will not fix it.
// If it were classified as retryable, one bad template would make every delivery waste three rounds of backoff.
func TestTemplateRenderFailsPermanently(t *testing.T) {
cfg := map[string]any{
"url": "https://example.com/hook",
"body_template": `{{.Items.`,
}
if err := (webhookChannel{}).Validate(cfg); err == nil {
t.Fatal("template syntax errors should be caught at save time")
}
// Even if validation is bypassed and delivery is attempted directly, it must be judged a permanent failure rather than retried over and over.
_, err := (webhookChannel{}).Send(context.Background(), cfg, singleMsg())
if err == nil || !IsPermanent(err) {
t.Fatalf("a bad template should be a permanent failure, got %v", err)
}
}
// TestTemplateCanOnlyProduceJSON covers the constraint that "the template render result must be valid JSON".
// It also blocks uses like "using the template to generate plain text to trigger other protocols".
func TestTemplateCanOnlyProduceJSON(t *testing.T) {
// A valid template passes.
ok := map[string]any{"url": "https://example.com/hook", "body_template": `{"t":{{json .Title}}}`}
if err := (webhookChannel{}).Validate(ok); err != nil {
t.Fatalf("a valid template should pass validation: %v", err)
}
// When the render result is not JSON it must be rejected (rather than sent out as-is).
bad := map[string]any{"url": "http://127.0.0.1:1/hook", "body_template": `not json {{.Count}}`}
_, err := (webhookChannel{}).Send(context.Background(), bad, singleMsg())
if err == nil || !IsPermanent(err) {
t.Fatalf("rendering non-JSON should be a permanent failure, got %v", err)
}
if !strings.Contains(err.Error(), "valid JSON") {
t.Errorf("the error message should indicate a JSON problem, got %v", err)
}
}