35168b9b83
- chat.SanitizeHTML/SanitizePlain decoded-then-escape text runs, so HTML entities the browser emits (e.g. for the space after a mention chip, or & for a typed "&") no longer render as literal " "/"&". Re-escaping keeps output XSS-safe. Adds regression tests. - Rename ./volumes -> ./.volumes so `go build/vet/test ./...` skip the mongo data dir (700-perm files); Makefile lint now gofmt's tracked files only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
84 lines
3.6 KiB
Go
84 lines
3.6 KiB
Go
package chat
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSanitizeHTML(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"plain text escaped", `hello <world> & "friends"`, "hello & "friends""},
|
|
{"existing entity not double-escaped", `a & b <ok>`, "a & b <ok>"},
|
|
{"mention span kept with sanitized uid", `<span class="mention" data-user-card="01ABCdef">@Jane Doe</span> hi`,
|
|
`<span class="mention" data-user-card="01ABCdef">@Jane Doe</span> hi`},
|
|
{"non-mention span dropped", `<span style="x">keep text</span>`, "keep text"},
|
|
{"allowed formatting kept", "<b>bold</b> <i>it</i> <u>u</u> <s>s</s>", "<b>bold</b> <i>it</i> <u>u</u> <s>s</s>"},
|
|
{"paragraph and lists", "<p>a</p><ul><li>x</li><li>y</li></ul>", "<p>a</p><ul><li>x</li><li>y</li></ul>"},
|
|
{"code and pre", "<pre><code>x := 1</code></pre>", "<pre><code>x := 1</code></pre>"},
|
|
{"blockquote", "<blockquote>q</blockquote>", "<blockquote>q</blockquote>"},
|
|
{"br void", "a<br>b<br/>c", "a<br>b<br>c"},
|
|
{"script stripped", `<script>alert(1)</script>hi`, "alert(1)hi"},
|
|
{"img dropped", `<img src=x onerror=alert(1)>safe`, "safe"},
|
|
{"event handlers stripped from allowed tag", `<b onclick="evil()">x</b>`, "<b>x</b>"},
|
|
{"style attr stripped", `<p style="position:fixed">x</p>`, "<p>x</p>"},
|
|
{"https link kept with rel", `<a href="https://x.y/z">l</a>`,
|
|
`<a href="https://x.y/z" rel="noopener noreferrer" target="_blank">l</a>`},
|
|
{"http link kept", `<a href="http://x.y">l</a>`,
|
|
`<a href="http://x.y" rel="noopener noreferrer" target="_blank">l</a>`},
|
|
{"javascript href removed", `<a href="javascript:alert(1)">l</a>`, "<a>l</a>"},
|
|
{"data href removed", `<a href="data:text/html,x">l</a>`, "<a>l</a>"},
|
|
{"single-quoted js href removed", `<a href='javascript:x'>l</a>`, "<a>l</a>"},
|
|
{"unquoted href", `<a href=https://ok.example>l</a>`,
|
|
`<a href="https://ok.example" rel="noopener noreferrer" target="_blank">l</a>`},
|
|
{"unclosed tags balanced", "<b>bold <i>both", "<b>bold <i>both</i></b>"},
|
|
{"stray close ignored", "</b>text", "text"},
|
|
{"mismatched nesting closed in order", "<b><i>x</b></i>", "<b><i>x</i></b>"},
|
|
{"case insensitive tags", "<B>x</B><SCRIPT>y</SCRIPT>", "<b>x</b>y"},
|
|
{"nested quotes attack", `<a href="https://x" "><script>alert(1)</script>">x</a>`,
|
|
`<a href="https://x" rel="noopener noreferrer" target="_blank">alert(1)">x</a>`},
|
|
{"incomplete tag escaped", "a <b", "a <b"},
|
|
{"empty input", "", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := SanitizeHTML(tt.in); got != tt.want {
|
|
t.Errorf("SanitizeHTML(%q)\n got %q\n want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSanitizeHTMLNeverEmitsDangerousOutput(t *testing.T) {
|
|
vectors := []string{
|
|
`<script>alert(1)</script>`,
|
|
`<img src=x onerror=alert(1)>`,
|
|
`<svg onload=alert(1)>`,
|
|
`<iframe src="https://evil"></iframe>`,
|
|
`<a href="javascript:alert(1)">x</a>`,
|
|
`<A HREF="JAVASCRIPT:alert(1)">x</A>`,
|
|
`<b onmouseover="alert(1)">x</b>`,
|
|
`<<script>script>alert(1)<</script>/script>`,
|
|
`<p><style>*{}</style></p>`,
|
|
`<form action="https://evil"><input></form>`,
|
|
}
|
|
for _, v := range vectors {
|
|
got := strings.ToLower(SanitizeHTML(v))
|
|
for _, bad := range []string{"<script", "onerror", "onload", "onmouseover",
|
|
"javascript:", "<iframe", "<svg", "<img", "<form", "<style", "<input"} {
|
|
if strings.Contains(got, bad) {
|
|
t.Errorf("vector %q produced dangerous output %q (contains %q)", v, got, bad)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSanitizePlain(t *testing.T) {
|
|
if got := SanitizePlain(`<b>x</b> & <script>y</script>`); got != "x & y" {
|
|
t.Errorf("SanitizePlain = %q", got)
|
|
}
|
|
}
|