- FormatHTML sanitizes the goldmark output with a bluemonday UGC policy extended only with what goldmark and the imported posts and categories use; unsafe markup is stripped, as Winter's Html::clean strips it - HTML the policy keeps whole is stored byte for byte (token comparison) - CheckHTML keeps its signature and 422 contract, refusing HTML the policy would change - the three regexes are gone: the CR-01 bypasses are neutralized and "Data:" or "metadata:" text is no longer refused (WR-01)
62 lines
2.6 KiB
Go
62 lines
2.6 KiB
Go
package classes
|
|
|
|
import "testing"
|
|
|
|
func TestCheckHTML(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
ok bool
|
|
}{
|
|
{"empty", "", true},
|
|
{"heading and link", `<h2>Cordyceps</h2><p>Plain <a href="https://example.test/a">link</a></p>`, true},
|
|
{"table", `<table><thead><tr><th>A</th></tr></thead><tbody><tr><td>1</td></tr></tbody></table>`, true},
|
|
{"polish date label", `<p>Data: 12 maja 2024</p>`, true},
|
|
{"metadata text", `<p>Some metadata: here</p>`, true},
|
|
{"quotes, apostrophes and nbsp", `<h2>Co to jest Lion's Mane? </h2><p>"superior herb" & inne</p>`, true},
|
|
{"pasted chat transcript", `<div data-message-author-role="assistant" data-message-id="cf51192a-db36-4816-8d29-b0606b53d289" dir="auto"><p>x</p></div>`, true},
|
|
{"script", `<script>alert(1)</script>`, false},
|
|
{"iframe", `<iframe src="https://example.test/"></iframe>`, false},
|
|
{"object", `<object data=x>`, false},
|
|
{"embed", `<embed src=x>`, false},
|
|
{"event handler", `<img src=x onerror="alert(1)">`, false},
|
|
{"event handler without a space", `<p><img src="x"onerror="alert(1)"></p>`, false},
|
|
{"slash-separated svg onload", "<div>\n<svg/onload=alert(1)>\n</div>", false},
|
|
{"javascript url", `<a href="javascript:alert(1)">`, false},
|
|
{"numeric entity colon", `<a href="javascript:alert(1)">x</a>`, false},
|
|
{"named entity colon", `<a href="javascript:alert(1)">x</a>`, false},
|
|
{"entity inside the scheme", `<a href="javascript:alert(1)">x</a>`, false},
|
|
{"tab inside the scheme", "<a href=\"java\tscript:alert(1)\">x</a>", false},
|
|
{"vbscript url mixed case", `<a href="VBScript:x">`, false},
|
|
{"data url", `<img src="data:image/png;base64,AAAA">`, false},
|
|
{"base", `<base href="https://evil.test/">`, false},
|
|
{"meta refresh", `<meta http-equiv="refresh" content="0;url=https://evil.test">`, false},
|
|
{"form", `<form action="https://evil.test"><input type=password></form>`, false},
|
|
{"style element", `<style>p { color: red }</style>`, false},
|
|
{"comment", `<p>a</p><!-- hidden -->`, false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := CheckHTML(tc.in)
|
|
if tc.ok && err != nil {
|
|
t.Fatalf("CheckHTML(%q) = %v, want nil", tc.in, err)
|
|
}
|
|
if !tc.ok && err == nil {
|
|
t.Fatalf("CheckHTML(%q) = nil, want an error", tc.in)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Every stored-XSS payload FormatHTML strips is refused by CheckHTML.
|
|
for _, tc := range unsafePayloads {
|
|
if tc.src[0] != '<' {
|
|
continue // markdown, not HTML
|
|
}
|
|
t.Run("payload "+tc.name, func(t *testing.T) {
|
|
if err := CheckHTML(tc.src); err == nil {
|
|
t.Fatalf("CheckHTML(%q) = nil, want an error", tc.src)
|
|
}
|
|
})
|
|
}
|
|
}
|