// SPDX-License-Identifier: AGPL-3.0-or-later package web import ( "strings" "testing" "bnema.dev/ermite/internal/domain" "bnema.dev/ermite/internal/ports/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) func renderMD(t *testing.T, src string) (string, []Heading) { t.Helper() h, outline, err := newMarkdown().render([]byte(src), "demo", "main", "docs") require.NoError(t, err) return string(h), outline } // AC4: raw HTML, javascript: links and SVG are neutralised. func TestMarkdownNeutralisation(t *testing.T) { t.Parallel() tests := []struct { name, src string absent []string present []string }{ { "raw html block", "\n\n
hi
\n", []string{" b c \n", []string{""}, []string{"

a "}, }, { "javascript link", "[x](javascript:alert(1)) [y](JaVaScRiPt:alert(1)) [z](java\tscript:alert(1))\n", []string{"javascript", "JaVaScRiPt", "href=\"java"}, []string{"

x y "}, }, { "data and other schemes", "[a](data:text/html,) [b](vbscript:x) [c](file:///etc/passwd) [d](mailto:a@b.c) [e](//evil.example/x) [f](ftp://h/x)\n", []string{"href=\"data", "vbscript", "file:", "mailto", "evil.example", "ftp:"}, []string{"

a b c d e f

"}, }, { "autolinks", " \n", []string{"href=\"javascript", "mailto:"}, []string{`href="https://ok.example/a"`, "me@example.org"}, }, { "svg and images", "![logo](x.svg) ![remote](https://evil.example/p.png) ![](data:image/svg+xml;base64,PHN2Zz4=)\n\n\n", []string{"[logo]`, `[remote]`, `[image]`}, }, { "escaped text", "# x & \"q\"\n\nplain tags & \"quotes\"\n", []string{"", ""}, []string{"&", """}, }, { "title injection", "[x](https://a.example \"t\\\" onmouseover=\\\"alert(1)\")\n", []string{"onmouseover=\"", " title="}, []string{`href="https://a.example"`}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() out, _ := renderMD(t, tt.src) for _, s := range tt.absent { assert.NotContains(t, out, s) } for _, s := range tt.present { assert.Contains(t, out, s) } // Whatever the input: no inline script, no event handler, no // style, no element that loads something. for _, banned := range []string{"b`, "alignment is an attribute, never a style") assert.NotContains(t, out, "style=") assert.Contains(t, out, "gone") assert.Contains(t, out, `type="checkbox"`) } func renderUntrustedMD(t *testing.T, src string) string { t.Helper() h, err := newMarkdown().renderUntrusted([]byte(src), "issue-desc-", nil) require.NoError(t, err) return string(h) } // An issue from the public form is rendered with the strictest policy. func TestMarkdownUntrusted(t *testing.T) { t.Parallel() tests := []struct { name, src string absent []string present []string }{ {"script", "\n", []string{" b\n", []string{"a "}}, {"javascript link", "[x](javascript:alert(1))\n", []string{"javascript", "href"}, []string{"

x

"}}, {"data link", "[x](data:text/html,)\n", []string{"data:", "href", " and \n", []string{"href=\"javascript"}, []string{`rel="nofollow noopener noreferrer ugc"`}}, {"autolink email", "\n", nil, []string{`href="mailto:a@example.org"`, "ugc"}}, {"bare url (GFM)", "see https://example.org/x now\n", nil, []string{`href="https://example.org/x"`, "ugc"}}, {"image is its alt text", "![pwn](https://evil.test/p.png)\n", []string{"[pwn]`}}, {"reference link to javascript", "[x][r]\n\n[r]: javascript:alert(1)\n", []string{"javascript", "href"}, nil}, {"entity-encoded scheme", "[x](javascript:alert(1))\n", []string{"avascript:", "href"}, nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() out := renderUntrustedMD(t, tt.src) for _, a := range tt.absent { assert.NotContains(t, out, a) } for _, p := range tt.present { assert.Contains(t, out, p) } }) } } func TestMarkdownUntrustedBigInputs(t *testing.T) { t.Parallel() long := strings.Repeat("a", 10000) out := renderUntrustedMD(t, long) assert.Contains(t, out, long) rows := "| a | b |\n|---|---|\n" + strings.Repeat("| x | [l](javascript:1) |\n", 500) out = renderUntrustedMD(t, rows) assert.NotContains(t, out, "") assert.NotContains(t, out, "javascript") assert.NotContains(t, out, "style=") deep := strings.Repeat("> ", 200) + "[x](javascript:1)\n" assert.NotContains(t, renderUntrustedMD(t, deep), "javascript") assert.NotContains(t, renderUntrustedMD(t, "[x](https://a.test/\"onmouseover=\"alert(1))\n"), "onmouseover=\"") } func TestNoteMarkdown(t *testing.T) { t.Parallel() tests := []struct { source string links int }{ {"^2 ^99 a^2 _^2 ^^2 1^2", 1}, {"`^2` [^2](https://example.org) [^2](javascript:alert(1))", 0}, {"```\n^2\n```", 0}, {"^2, ^2\n^2", 3}, } for _, tt := range tests { t.Run(tt.source, func(t *testing.T) { t.Parallel() h, err := newMarkdown().renderNote([]byte(tt.source), "demo", "main", "c1-", refResolver{notes: func(n int64) bool { return n == 2 }}) require.NoError(t, err) assert.Equal(t, tt.links, strings.Count(string(h), `href="#c2"`), string(h)) assert.NotContains(t, string(h), `href="#c99"`) assert.NotContains(t, string(h), "javascript:") }) } h, err := newMarkdown().renderNote([]byte("# Heading\n\n\n\n**bold** ^2"), "demo", "main", "c1-", refResolver{notes: func(n int64) bool { return n == 2 }}) require.NoError(t, err) assert.Contains(t, string(h), `id="c1-heading"`) assert.Contains(t, string(h), "bold") assert.NotContains(t, string(h), "#3`, `title="merge request 4"`}, []string{"/issues/9", "#9"}}, {"abc#3 _#3 ##3 1#3", nil, []string{"\n", false}, {"empty", "", "\n", true}, {"injection", `go" onmouseover=x`, "package main\n", false}, {"oversize", "go", strings.Repeat("x", markdownBlockBytes+1) + "\n", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { src := "```" + tt.info + "\n" + tt.code + "```\n" for _, untrusted := range []bool{false, true} { var h string if untrusted { out, err := newMarkdown().renderUntrusted([]byte(src), "test-", nil) require.NoError(t, err) h = string(out) } else { h, _ = renderMD(t, src) } assert.Equal(t, tt.highlighted, strings.Contains(h, `
`))
				assert.NotContains(t, h, "