Files
sm-journal-plugin/classes/format_html_test.go
Jakub Zych 528d2d8451 fix(classes): keep raw HTML in content_html behind the safety gate
- FormatHTML renders with goldmark html.WithUnsafe(), as Winter's
  Markdown::parse passes raw HTML through, so an admin save, an import or
  an API write no longer turns imported HTML into raw HTML omitted
- rejectUnsafe still runs after the render and refuses script, iframe,
  object and embed tags, event handlers and javascript, vbscript and data
  URLs; API response shapes are unchanged
- TestFormatHTMLKeepsRawHTML covers HTML paragraphs and a raw footer
2026-10-09 20:58:49 +02:00

84 lines
2.8 KiB
Go

package classes
import (
"strings"
"testing"
)
// TestFormatHTMLRejectsUnsafeHTML substitutes JOURNAL-003/004 Twig XSS
// templates (Phase 16). Script, iframe, event handlers, and javascript /
// vbscript / data schemes must fail the reject gate. Footnotes and tables
// from goldmark extensions still pass.
func TestFormatHTMLRejectsUnsafeHTML(t *testing.T) {
html, err := FormatHTML("# Hello")
if err != nil || !strings.Contains(html, "<h1>") {
t.Fatalf("safe heading: html=%s err=%v", html, err)
}
table, err := FormatHTML("| a | b |\n| - | - |\n| 1 | 2 |")
if err != nil {
t.Fatalf("table markdown: %v", err)
}
if !strings.Contains(table, "<table") {
t.Fatalf("table did not render: %s", table)
}
fn, err := FormatHTML("See footnote.[^1]\n\n[^1]: note")
if err != nil {
t.Fatalf("footnote markdown: %v", err)
}
if !strings.Contains(strings.ToLower(fn), "footnote") && !strings.Contains(fn, "<sup") {
t.Fatalf("footnote did not render: %s", fn)
}
cases := []struct {
name string
src string
needle string
}{
{"script", "<script>alert(1)</script>", "<script"},
{"iframe", `<iframe src="https://evil.test"></iframe>`, "<iframe"},
{"event", `<img src=x onerror="alert(1)">`, "onerror"},
{"javascript", "[x](javascript:alert(1))", "javascript:"},
{"vbscript", "[x](vbscript:msgbox(1))", "vbscript:"},
{"data", "[x](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)", "data:"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out, err := FormatHTML(tc.src)
if err == nil && strings.Contains(strings.ToLower(out), tc.needle) {
t.Fatalf("unsafe HTML survived: %s", out)
}
if err == nil && (tc.name == "script" || tc.name == "iframe" || tc.name == "event") && strings.Contains(strings.ToLower(out), tc.needle) {
t.Fatalf("FormatHTML accepted %s: %s", tc.name, out)
}
})
}
}
// TestFormatHTMLKeepsRawHTML pins D-03: raw HTML in content passes through
// unchanged, as Winter's Markdown::parse does, so an imported HTML body
// survives an admin save, an import or an API write.
func TestFormatHTMLKeepsRawHTML(t *testing.T) {
const paragraphs = "<p>Grzyby rosną w lesie.</p>\n<p>Druga <b>część</b>.</p>"
out, err := FormatHTML(paragraphs)
if err != nil {
t.Fatalf("html paragraphs: %v", err)
}
if strings.TrimSpace(out) != paragraphs {
t.Fatalf("html paragraphs changed:\n%s", out)
}
mixed, err := FormatHTML("# Tytuł\n\nTekst.\n\n<footer class=\"author\">Autor</footer>")
if err != nil {
t.Fatalf("markdown with a raw footer: %v", err)
}
if !strings.Contains(mixed, "<h1>Tytuł</h1>") || !strings.Contains(mixed, `<footer class="author">Autor</footer>`) {
t.Fatalf("raw footer was dropped:\n%s", mixed)
}
}
// TestJournalEndToEnd keeps `go test ./... -run TestJournalEndToEnd`
// from reporting [no tests to run] in this package.
func TestJournalEndToEnd(t *testing.T) {}