[READ-ONLY] Mirror of https://github.com/darccio/zas. Most simple static website generator in Golang.
go static-site-generation static-site-generator
Something went wrong. Try again.
zas embed_containment_test.go
8.6 kB · 248 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249package zas
import ( "os" "path/filepath" "strings" "testing"
"github.com/PuerkitoBio/goquery")
// <embed src="..."> was read with os.ReadFile(src) and no check that the// result stayed inside the site root, so an absolute src or a "../"// traversal in site content could pull the contents of any file the build// process can read into the published output. resolveEmbedSrc closes that// off for all three embed handlers (Markdown, Plain, Html); these tests// cover both escape shapes for each handler, and confirm a legitimate// embed of a file in a site subdirectory still works.
type embedHandlerCase struct { name string typ string relPath string content string call func(*Generator, *goquery.Selection, *goquery.Document, *ZasData) error want string}
var embedHandlerCases = []embedHandlerCase{ {"Markdown", "text/markdown", "note.md", "# Note\n\nHello from markdown.\n", (*Generator).markdown, "Hello from markdown."}, {"Plain", "text/plain", "note.txt", "hello from plain", (*Generator).plain, "hello from plain"}, {"Html", "text/html", "note.html", "<p>hello from html</p>", (*Generator).html, "hello from html"},}
func embedDocFor(t *testing.T, src, typ string) *goquery.Document { t.Helper() doc, err := goquery.NewDocumentFromReader(strings.NewReader( `<div><embed src="` + src + `" type="` + typ + `"></div>`)) if err != nil { t.Fatal(err) } return doc}
func TestEmbedHandlersRejectAbsolutePathOutsideRoot(t *testing.T) { for _, tc := range embedHandlerCases { t.Run(tc.name, func(t *testing.T) { base := t.TempDir() root := filepath.Join(base, "site") if err := os.Mkdir(root, 0o755); err != nil { t.Fatal(err) } secret := filepath.Join(base, tc.relPath) if err := os.WriteFile(secret, []byte(tc.content), 0o644); err != nil { t.Fatal(err) } t.Chdir(root)
doc := embedDocFor(t, secret, tc.typ) gen := &Generator{} err := tc.call(gen, doc.Find("embed"), doc, &ZasData{}) if err == nil { t.Fatalf("%s() with an absolute src outside the site root: want error, got nil", tc.name) } if doc.Find("embed").Length() != 1 { t.Fatalf("%s() rejected the embed but still consumed the <embed> tag", tc.name) } }) }}
func TestEmbedHandlersRejectParentTraversal(t *testing.T) { for _, tc := range embedHandlerCases { t.Run(tc.name, func(t *testing.T) { base := t.TempDir() root := filepath.Join(base, "site") if err := os.Mkdir(root, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(base, tc.relPath), []byte(tc.content), 0o644); err != nil { t.Fatal(err) } t.Chdir(root)
traversal := filepath.Join("..", tc.relPath) doc := embedDocFor(t, traversal, tc.typ) gen := &Generator{} err := tc.call(gen, doc.Find("embed"), doc, &ZasData{}) if err == nil { t.Fatalf(`%s() with a src escaping the site root via "../": want error, got nil`, tc.name) } if doc.Find("embed").Length() != 1 { t.Fatalf("%s() rejected the embed but still consumed the <embed> tag", tc.name) } }) }}
func TestEmbedHandlersAllowNestedSubdirectory(t *testing.T) { for _, tc := range embedHandlerCases { t.Run(tc.name, func(t *testing.T) { t.Chdir(t.TempDir()) if err := os.Mkdir("partials", 0o755); err != nil { t.Fatal(err) } relPath := filepath.Join("partials", tc.relPath) if err := os.WriteFile(relPath, []byte(tc.content), 0o644); err != nil { t.Fatal(err) }
doc := embedDocFor(t, relPath, tc.typ) gen := &Generator{} if err := tc.call(gen, doc.Find("embed"), doc, &ZasData{}); err != nil { t.Fatalf("%s() for a file in a site subdirectory: error = %v, want nil", tc.name, err) } if got := doc.Find("div").Text(); !strings.Contains(got, tc.want) { t.Fatalf("%s() output = %q, want it to contain %q", tc.name, got, tc.want) } }) }}
// TestGenerateRejectsEmbedEscapingSiteRootViaAbsolutePath and// TestGenerateRejectsEmbedEscapingSiteRootViaTraversal drive the full// Generator.Run pipeline (as opposed to calling a handler method// directly) to confirm a rejected embed surfaces as an ordinary per-file// generation error - the same path any other render failure already takes// - rather than a panic or a silent success with leaked file content in// the deploy output.
func TestGenerateRejectsEmbedEscapingSiteRootViaAbsolutePath(t *testing.T) { base := t.TempDir() root := filepath.Join(base, "site") if err := os.Mkdir(root, 0o755); err != nil { t.Fatal(err) } copyFixture(t, "site", root) t.Chdir(root)
secret := filepath.Join(base, "secret.txt") if err := os.WriteFile(secret, []byte("do not leak"), 0o644); err != nil { t.Fatal(err) } leak := `<embed src="` + secret + `" type="text/plain">` + "\n" if err := os.WriteFile("leak.html", []byte(leak), 0o644); err != nil { t.Fatal(err) }
if err := generate(t); err == nil { t.Fatal("generate() with a page embedding an absolute path outside the site root: want error, got nil") } assertDeployMissing(t, "leak.html")}
func TestGenerateRejectsEmbedEscapingSiteRootViaTraversal(t *testing.T) { base := t.TempDir() root := filepath.Join(base, "site") if err := os.Mkdir(root, 0o755); err != nil { t.Fatal(err) } copyFixture(t, "site", root) t.Chdir(root)
if err := os.WriteFile(filepath.Join(base, "secret.txt"), []byte("do not leak"), 0o644); err != nil { t.Fatal(err) } leak := `<embed src="../secret.txt" type="text/plain">` + "\n" if err := os.WriteFile("leak.html", []byte(leak), 0o644); err != nil { t.Fatal(err) }
if err := generate(t); err == nil { t.Fatal(`generate() with a page embedding "../secret.txt": want error, got nil`) } assertDeployMissing(t, "leak.html")}
// A page-body embed now resolves relative to the embedding page's own// directory (see embed_page_relative_test.go) rather than always the site// root, so containment has to keep working against that new base too: a// "../" sequence deep enough to walk past the site root itself must still// be rejected, even though it's now relative to the page's own// subdirectory instead of the root the old, pre-page-relative code always// used as its base.func TestGenerateRejectsPageRelativeEmbedEscapingSiteRoot(t *testing.T) { base := t.TempDir() root := filepath.Join(base, "site") if err := os.Mkdir(root, 0o755); err != nil { t.Fatal(err) } copyFixture(t, "site", root) t.Chdir(root)
if err := os.WriteFile(filepath.Join(base, "secret.txt"), []byte("do not leak"), 0o644); err != nil { t.Fatal(err) } if err := os.Mkdir("section", 0o755); err != nil { t.Fatal(err) } // "section/leak.html" is one directory below the site root, so a // single ".." from there reaches the root itself (see the companion // success case below) - a second ".." is what walks past it into base, // where secret.txt lives. leak := `<embed src="../../secret.txt" type="text/plain">` + "\n" if err := os.WriteFile(filepath.Join("section", "leak.html"), []byte(leak), 0o644); err != nil { t.Fatal(err) }
if err := generate(t); err == nil { t.Fatal(`generate() with "section/leak.html" embedding "../../secret.txt": want error, got nil`) } assertDeployMissing(t, "section/leak.html")}
// Companion to the rejection above: a page-relative "../" that stays// inside the site root (here, one level up from "section/" lands exactly// on the root) must still be allowed - containment rejects paths that// escape the site root, not every ".." on principle, the same way a// relative link in a deployed browser page can legitimately walk up to a// shared sibling directory.func TestGenerateAllowsPageRelativeEmbedOneLevelUpStillInsideRoot(t *testing.T) { base := t.TempDir() root := filepath.Join(base, "site") if err := os.Mkdir(root, 0o755); err != nil { t.Fatal(err) } copyFixture(t, "site", root) t.Chdir(root)
if err := os.WriteFile("shared.txt", []byte("shared content"), 0o644); err != nil { t.Fatal(err) } if err := os.Mkdir("section", 0o755); err != nil { t.Fatal(err) } reader := "<!-- title: Reader -->\n" + `<embed src="../shared.txt" type="text/plain">` + "\n" if err := os.WriteFile(filepath.Join("section", "reader.html"), []byte(reader), 0o644); err != nil { t.Fatal(err) }
if err := generate(t); err != nil { t.Fatalf(`generate() with "section/reader.html" embedding "../shared.txt": error = %v, want nil`, err) } got := readDeploy(t, "section/reader.html") if !strings.Contains(got, "shared content") { t.Fatalf("deployed section/reader.html = %q, want it to contain %q", got, "shared content") }}