From 502bdf64817928997e2afdd67b032b4a2bf99243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Sun, 9 Aug 2026 18:16:56 +0200 Subject: [PATCH] fix(generate): validate embed plugin method signature before Call handleEmbedTags Title-cases the mimetypes: config value and looks it up with MethodByName on *Generator, then calls it with the fixed 3-argument embed-plugin signature. Any exported Generator method with a different signature is reachable from config data - e.g. mimetypes: {text/x-t: run} resolves to the real, 0-argument Run() error method, and method.Call panics with "too many input arguments". The method == reflect.ValueOf(nil) validity check was also fragile compared to the idiomatic method.IsValid(). isEmbedPluginMethod now checks IsValid() plus the exact parameter and return types before Call is ever reached, falling back to the external-plugin path for anything that doesn't match - the same behavior as when the method isn't found at all. --- embed_dispatch_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++ generate.go | 30 +++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 embed_dispatch_test.go diff --git a/embed_dispatch_test.go b/embed_dispatch_test.go new file mode 100644 index 0000000..94908d6 --- /dev/null +++ b/embed_dispatch_test.go @@ -0,0 +1,48 @@ +package zas + +import ( + "reflect" + "strings" + "testing" + + "github.com/PuerkitoBio/goquery" +) + +// B6: config data must not be able to pick an arbitrary Generator method via +// reflection and panic method.Call with a mismatched arity/signature. + +func TestIsEmbedPluginMethod(t *testing.T) { + gen := &Generator{} + + if valid := reflect.ValueOf(gen).MethodByName("Markdown"); !isEmbedPluginMethod(valid) { + t.Fatal("isEmbedPluginMethod(Markdown) = false, want true") + } + // Run() error is a real exported Generator method, but with the wrong + // arity (0 args) for embed dispatch - must be rejected, not panic. + if wrongArity := reflect.ValueOf(gen).MethodByName("Run"); isEmbedPluginMethod(wrongArity) { + t.Fatal("isEmbedPluginMethod(Run) = true, want false") + } + if notFound := reflect.ValueOf(gen).MethodByName("DoesNotExist"); isEmbedPluginMethod(notFound) { + t.Fatal("isEmbedPluginMethod(missing method) = true, want false") + } +} + +func TestHandleEmbedTagsFallsBackOnWrongArityMethod(t *testing.T) { + // Reproduces the audit repro: mimetypes: {text/x-t: run} resolves to the + // real, exported Run() error method, which has the wrong signature for + // embed dispatch. Must fall back to the external plugin path instead of + // panicking in reflect.Value.Call. The fallback path itself is a no-op + // here (see audit A1, out of scope), so simply completing without a + // panic is the regression this guards. + gen := &Generator{ + Config: ConfigSection{ + "mimetypes": ConfigSection{"text/x-t": "run"}, + }, + } + doc, err := goquery.NewDocumentFromReader(strings.NewReader( + ``)) + if err != nil { + t.Fatal(err) + } + _ = gen.handleEmbedTags(doc, &ZasData{}) +} diff --git a/generate.go b/generate.go index 0e115fc..dab1ab8 100644 --- a/generate.go +++ b/generate.go @@ -576,7 +576,7 @@ func (gen *Generator) handleEmbedTags(doc *goquery.Document, data *ZasData) (err } plugin := gen.resolveMIMETypePlugin(typ) method := reflect.ValueOf(gen).MethodByName(cases.Title(language.English).String(plugin)) - if method == reflect.ValueOf(nil) { + if !isEmbedPluginMethod(method) { err = gen.handleMIMETypePlugin(e, doc) } else { args := make([]reflect.Value, 3) @@ -598,6 +598,34 @@ func (gen *Generator) handleEmbedTags(doc *goquery.Document, data *ZasData) (err return } +/* + * Reports whether method is a valid embed-plugin dispatch target: a method + * with the exact (e *goquery.Selection, doc *goquery.Document, data *ZasData) error + * signature. Config data chooses the method name (see resolveMIMETypePlugin), + * so any exported Generator method is reachable by MethodByName and must be + * shape-checked before Call to avoid a reflect panic on arity/type mismatch. + */ +func isEmbedPluginMethod(method reflect.Value) bool { + if !method.IsValid() { + return false + } + want := []reflect.Type{ + reflect.TypeFor[*goquery.Selection](), + reflect.TypeFor[*goquery.Document](), + reflect.TypeFor[*ZasData](), + } + t := method.Type() + if t.NumIn() != len(want) || t.NumOut() != 1 { + return false + } + for i, w := range want { + if t.In(i) != w { + return false + } + } + return true +} + type bufErr struct { buffer []byte err error -- 2.51.2