Artifact ab5f054b5a381964f2e985ebbeb06e84e47d4d848116f4c0d02c3321444591cb:
- File
internal/markup/markup_test.go
— part of check-in
[689d0692d7]
at
2026-08-15 12:16:01
on branch trunk
— highlight: chroma wrapper, fenced-code + file-view highlighting
Cleanroom (their wrapper is settings-coupled); classes only, both themes in one stylesheet; sanitizer learns chroma token classes. (user: dmestas, size: 6798) [annotate] [blame] [check-ins using]
package markup import ( "encoding/json" "os" "strings" "testing" "golang.org/x/net/html" "golang.org/x/net/html/atom" ) // --- conformance corpus (third_party/sanitizer-corpus, see ledger) --- type corpusCase struct { Name string `json:"name"` Input string `json:"input"` Deny []string `json:"deny"` Allow []string `json:"allow"` } type corpusFile struct { Source string `json:"source"` Cases []corpusCase `json:"cases"` } func TestConformanceCorpus(t *testing.T) { for _, name := range []string{"gitea", "forgejo"} { raw, err := os.ReadFile("../../third_party/sanitizer-corpus/" + name + ".json") if err != nil { t.Fatal(err) } var cf corpusFile if err := json.Unmarshal(raw, &cf); err != nil { t.Fatalf("%s: %v", name, err) } if len(cf.Cases) < 10 { t.Fatalf("%s: suspiciously small corpus (%d)", name, len(cf.Cases)) } for _, c := range cf.Cases { out := Sanitize(c.Input) for _, bad := range c.Deny { if strings.Contains(out, bad) { t.Errorf("%s/%s: output contains %q:\n%s", name, c.Name, bad, out) } } for _, good := range c.Allow { if !strings.Contains(out, good) { t.Errorf("%s/%s: output lost %q:\n%s", name, c.Name, good, out) } } } } } // --- rendering --- func TestRenderMarkdown(t *testing.T) { out, err := Render([]byte("# Title\n\nSome *emphasis* and `code`.\n\n- [ ] task\n"), nil) if err != nil { t.Fatal(err) } s := string(out) for _, want := range []string{"<h1", "<em>emphasis</em>", "<code>code</code>", `type="checkbox"`} { if !strings.Contains(s, want) { t.Errorf("missing %q in:\n%s", want, s) } } } func TestRenderStripsRawHTMLAttacks(t *testing.T) { out, err := Render([]byte("hello <script>alert(1)</script> <a href=\"javascript:x\" onclick=\"y\">link</a>\n"), nil) if err != nil { t.Fatal(err) } s := string(out) for _, bad := range []string{"<script", "javascript:", "onclick"} { if strings.Contains(s, bad) { t.Errorf("attack survived rendering: %q in\n%s", bad, s) } } } func TestRenderFencedCodeHighlights(t *testing.T) { out, err := Render([]byte("```go\npackage main\n```\n"), nil) if err != nil { t.Fatal(err) } s := string(out) // Chroma emits classed spans; the sanitizer must keep them and the // keyword must be inside one. if !strings.Contains(s, `<span class="kn">package</span>`) && !strings.Contains(s, `<span class="k">package</span>`) { t.Fatalf("fenced code not highlighted:\n%s", s) } } func TestRenderFencedCodeUnknownLanguageStaysPlain(t *testing.T) { out, err := Render([]byte("```zzznotalang\nhello\n```\n"), nil) if err != nil { t.Fatal(err) } if !strings.Contains(string(out), "hello") { t.Fatalf("code content lost:\n%s", out) } } func TestRenderTable(t *testing.T) { out, err := Render([]byte("| a | b |\n|---|---|\n| 1 | 2 |\n"), nil) if err != nil { t.Fatal(err) } if !strings.Contains(string(out), "<table>") { t.Fatalf("GFM table lost:\n%s", out) } } // --- resolver seam --- type fakeResolver struct{} func (fakeResolver) ResolveHash(prefix string) string { if strings.HasPrefix(prefix, "abc123") { return "/checkin/" + prefix } return "" } func (fakeResolver) ResolveWiki(name string) string { if name == "Design" { return "/wiki/Design" } return "" } func TestResolveWikiRef(t *testing.T) { out, err := Render([]byte("See [[Design]] for details.\n"), fakeResolver{}) if err != nil { t.Fatal(err) } if !strings.Contains(string(out), `<a href="/wiki/Design">Design</a>`) { t.Fatalf("wiki ref not resolved:\n%s", out) } } func TestResolveUnknownWikiStaysText(t *testing.T) { out, err := Render([]byte("See [[Nowhere]].\n"), fakeResolver{}) if err != nil { t.Fatal(err) } if strings.Contains(string(out), "<a") { t.Fatalf("unresolvable ref linked:\n%s", out) } if !strings.Contains(string(out), "[[Nowhere]]") { t.Fatalf("unresolved ref text lost:\n%s", out) } } func TestResolveHashRef(t *testing.T) { out, err := Render([]byte("Fixed in abc123def456.\n"), fakeResolver{}) if err != nil { t.Fatal(err) } if !strings.Contains(string(out), `<a href="/checkin/abc123def456">abc123def456</a>`) { t.Fatalf("hash ref not resolved:\n%s", out) } } func TestResolveSkipsCodeSpans(t *testing.T) { out, err := Render([]byte("`abc123def456` and [[Design]] in code: `[[Design]]`\n"), fakeResolver{}) if err != nil { t.Fatal(err) } s := string(out) if strings.Contains(s, `<code><a`) || strings.Contains(s, "code><a") { t.Fatalf("resolved inside code span:\n%s", s) } if !strings.Contains(s, `<a href="/wiki/Design">Design</a>`) { t.Fatalf("prose ref should still resolve:\n%s", s) } } func TestResolverOutputStillSafe(t *testing.T) { // A hostile resolver URL must not smuggle scripts — Render output // re-enters pages as trusted HTML, so the seam is load-bearing. out, err := Render([]byte("[[Design]]\n"), evilResolver{}) if err != nil { t.Fatal(err) } if strings.Contains(string(out), "<script") { t.Fatalf("resolver injected markup:\n%s", out) } } type evilResolver struct{} func (evilResolver) ResolveHash(string) string { return "" } func (evilResolver) ResolveWiki(string) string { return `"><script>alert(1)</script>` } // --- fuzz --- func FuzzSanitize(f *testing.F) { f.Add(`<a href="javascript:alert(1)" onmouseover=x>y</a>`) f.Add("<scrİpt>hi</scrİpt>") f.Add(`<img src=x onerror=alert(1)>`) f.Add(`<code class="language-go ui modal">x</code>`) f.Add("plain **markdown** text") f.Fuzz(func(t *testing.T, in string) { // The oracle parses the output: dangerous constructs must not // exist as MARKUP. (Substring checks false-positive on inputs // that are plain text like "onmouseover=0".) body := &html.Node{Type: html.ElementNode, Data: "body", DataAtom: atom.Body} nodes, err := html.ParseFragment(strings.NewReader(Sanitize(in)), body) if err != nil { t.Fatalf("output unparseable for %q: %v", in, err) } var walk func(*html.Node) walk = func(n *html.Node) { if n.Type == html.ElementNode { switch n.Data { case "script", "style", "iframe", "object", "embed", "form": t.Fatalf("policy leaked <%s> for input %q", n.Data, in) } for _, a := range n.Attr { if strings.HasPrefix(strings.ToLower(a.Key), "on") { t.Fatalf("policy leaked %s= on <%s> for input %q", a.Key, n.Data, in) } if a.Key == "href" || a.Key == "src" { v := strings.ToLower(strings.TrimSpace(a.Val)) if strings.HasPrefix(v, "javascript:") || strings.HasPrefix(v, "vbscript:") || strings.HasPrefix(v, "data:") { t.Fatalf("policy leaked %s=%q for input %q", a.Key, a.Val, in) } } } } for c := n.FirstChild; c != nil; c = c.NextSibling { walk(c) } } for _, n := range nodes { walk(n) } }) }