// 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](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", "  \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{"\n", []string{")\n", []string{"data:", "href", "\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), "\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, "