package seaglass_test import ( "slices" "strings" "testing" "git.golem15.com/golem15/summercms/modules/seaglass" ) // TestSanitizeByteStable pins D-10's byte-stability promise: HTML the policy // keeps whole comes back byte for byte, with attributes in their source order, // entities as written, whitespace and quoting untouched, and a zero report. func TestSanitizeByteStable(t *testing.T) { for _, html := range []string{ "", "plain text", "

One paragraph.

", "

Mushrooms can help.

\n\n

Second.

", `attributes in source order`, `relative mail tel`, `

Entities: & <tag> "q" 's'   © — —

`, `

Raw text quotes: It's "fine" & done

`, `

single quotes stay

`, `

upper-case attribute name

`, "", "
  1. a
    1. nested
", `

heading

h3

quote

`, `

line
break
self-closed

`, `An image`, `
h
c
`, `

styled

`, `
Tea
`, `

1

note

`, `
if a < b { return }
`, "

Zażółć gęślą jaźń — unicode “quotes”

", } { got, report := seaglass.SanitizeReport(html) if got != html { t.Errorf("SanitizeReport(%q) = %q, want the input byte for byte", html, got) } if report.Changed || report.ElementCount != 0 || report.AttributeCount != 0 || report.Comments != 0 || len(report.Elements) != 0 || len(report.Attributes) != 0 { t.Errorf("SanitizeReport(%q) report = %+v, want zero", html, report) } if out := seaglass.Sanitize(html); out != html { t.Errorf("Sanitize(%q) = %q", html, out) } if err := seaglass.Check(html); err != nil { t.Errorf("Check(%q) = %v", html, err) } } } // TestSanitizeReport pins the removal report: element, attribute and comment // names and counts, the ten-name cap with its exact counts, a repeated // attribute, a style that lost a property, and the sanitized output itself. func TestSanitizeReport(t *testing.T) { for _, tc := range []struct { name string in, out string elements []string elemCount int attributes []string attrCount int comments int }{ { name: "element, attribute and comments", in: `

Hi

`, out: `

Hi

`, elements: []string{"script"}, elemCount: 1, attributes: []string{"onclick"}, attrCount: 1, comments: 2, }, { name: "names are distinct in first-seen order, counts are exact", in: `

1

2

`, out: `

1

2

`, elements: []string{"script"}, elemCount: 2, attributes: []string{"onclick", "onmouseover"}, attrCount: 3, }, { name: "ten names then an ellipsis, the count stays exact", in: `
`, out: "", elements: []string{"iframe", "object", "embed", "form", "input", "base", "meta", "link", "svg", "math", "…"}, elemCount: 12, }, { name: "a repeated attribute counts as removed", in: `x`, out: `x`, attributes: []string{"href"}, attrCount: 1, }, { name: "a style that lost a property counts under style", in: `

x

`, out: `

x

`, attributes: []string{"style"}, attrCount: 1, }, { name: "target and rel are removed", in: `x`, out: `x`, attributes: []string{"target", "rel"}, attrCount: 2, }, { name: "a link without an allowed href loses its tag, not its text", in: `

x y

`, out: `

x y

`, elements: []string{"a"}, elemCount: 1, }, { name: "only comments", in: `

a

`, out: `

a

`, comments: 1, }, } { t.Run(tc.name, func(t *testing.T) { got, r := seaglass.SanitizeReport(tc.in) if got != tc.out { t.Fatalf("output = %q, want %q", got, tc.out) } if !r.Changed { t.Fatal("Changed = false") } if !slices.Equal(r.Elements, tc.elements) || r.ElementCount != tc.elemCount { t.Fatalf("elements = %v (%d), want %v (%d)", r.Elements, r.ElementCount, tc.elements, tc.elemCount) } if !slices.Equal(r.Attributes, tc.attributes) || r.AttributeCount != tc.attrCount { t.Fatalf("attributes = %v (%d), want %v (%d)", r.Attributes, r.AttributeCount, tc.attributes, tc.attrCount) } if r.Comments != tc.comments { t.Fatalf("comments = %d, want %d", r.Comments, tc.comments) } if seaglass.Sanitize(tc.in) != tc.out { t.Fatal("Sanitize differs from SanitizeReport") } // The sanitized output is itself clean and stable. again, r2 := seaglass.SanitizeReport(got) if again != got || r2.Changed { t.Fatalf("sanitizing the output changed it: %q %+v", again, r2) } }) } } // TestSanitizeBypassCorpus runs the payloads the bluemonday decision notes // list as bypasses of the regex gate it replaced (no space before an event // handler, entity-encoded and whitespace-split javascript schemes, svg // onload, MathML xlink:href, base, meta refresh, form, data and vbscript // URLs, a repeated href) plus the ordinary unsafe elements: each comes back // without the dangerous construct, with Changed set, and Check refuses it. func TestSanitizeBypassCorpus(t *testing.T) { for _, tc := range []struct { payload string banned []string }{ {``, []string{"onerror", "alert"}}, {``, []string{"onerror", "alert"}}, {`

x

`, []string{"onmouseover", "alert"}}, {``, []string{"x`, []string{"href", "javascript", "alert"}}, {`x`, []string{"href", "javascript", "alert"}}, {`x`, []string{"href", "avascript", "alert"}}, {"x", []string{"href", "script", "alert"}}, {`x`, []string{"href", "javascript", "alert"}}, {`x`, []string{"`, []string{"`, []string{"`, []string{"x`, []string{"href", "vbscript"}}, {`x`, []string{"javascript", "alert"}}, {`
x
`, []string{"style", "javascript"}}, {``, []string{"x

`, []string{"x

`, []string{"`, []string{" %q", tc.payload, got) } if err := seaglass.Check(tc.payload); err == nil { t.Fatal("Check accepted the payload") } }) } } // TestCheck pins Check: nil when the policy keeps the HTML whole (including // text that only looks like a scheme, the WR-01 false refusals), an error // prefixed "seaglass: " when it would change anything. func TestCheck(t *testing.T) { for _, html := range []string{ "", "

Data: 12 maja 2024, metadata: none

", "

javascript: is just a word here

", `

It's "fine"

`, `call`, } { if err := seaglass.Check(html); err != nil { t.Errorf("Check(%q) = %v", html, err) } } for _, html := range []string{ "", `dup`, `

x

`, `ftp`, } { err := seaglass.Check(html) if err == nil || !strings.HasPrefix(err.Error(), "seaglass: ") { t.Errorf("Check(%q) = %v, want a seaglass error", html, err) } } }