diff --git a/data.go b/data.go index 5c808d0..e866cf6 100644 --- a/data.go +++ b/data.go @@ -51,6 +51,9 @@ type ZasData struct { // onto the layout's element since Body only carries the source // body's inner HTML, not the element itself. bodyAttrs map[string]string + // Tracks embed nesting depth for this render, guarding against a self- + // or mutually-embedding file recursing without bound. + embedDepth int } /* diff --git a/embed_depth_test.go b/embed_depth_test.go new file mode 100644 index 0000000..2a9f459 --- /dev/null +++ b/embed_depth_test.go @@ -0,0 +1,33 @@ +package zas + +import ( + "os" + "strings" + "testing" + + "github.com/melvinmt/gt" +) + +// A file that embeds itself (directly, or through a cycle of mutually- +// embedding files) recurses through parseAndReplace/handleEmbedTags without +// bound now that raw HTML - including tags written in a .md source - +// is no longer dropped by the Markdown converter. This must fail with an +// error instead of exhausting the goroutine's stack. + +func TestRenderMarkdownSelfEmbedReturnsError(t *testing.T) { + t.Chdir(t.TempDir()) + if err := os.WriteFile("self.md", []byte(``+"\n"), 0o644); err != nil { + t.Fatal(err) + } + gen := &Generator{ + Config: ConfigSection{"mimetypes": ConfigSection{"text/markdown": "markdown"}}, + I18n: >.Build{Index: gt.Strings{}, Origin: "en"}, + } + err := gen.renderMarkdown("self.md") + if err == nil { + t.Fatal("renderMarkdown() on a self-embedding file: want error, got nil") + } + if !strings.Contains(err.Error(), "embed nesting") { + t.Fatalf("error = %v, want it to report excessive embed nesting", err) + } +} diff --git a/generate.go b/generate.go index 4fadd20..678cb54 100644 --- a/generate.go +++ b/generate.go @@ -184,7 +184,19 @@ func (gen *Generator) Generate(path string, data *ZasData) (err error) { return } +// maxEmbedDepth bounds how many levels of an entry file may nest. +// Markdown and HTML embed handlers call back into parseAndReplace for +// whatever they embed, so a file that embeds itself (directly, or through a +// cycle of mutually-embedding files) would otherwise recurse until the +// goroutine's stack is exhausted. +const maxEmbedDepth = 20 + func (gen *Generator) parseAndReplace(processed bytes.Buffer, data *ZasData) (doc *goquery.Document, err error) { + if data.embedDepth >= maxEmbedDepth { + return nil, fmt.Errorf("embed nesting deeper than %d levels; check for a self- or mutually-embedding file", maxEmbedDepth) + } + data.embedDepth++ + defer func() { data.embedDepth-- }() // Here we manipulate its result. doc, err = goquery.NewDocumentFromReader(&processed) if err != nil {