Files

110 lines
3.1 KiB
Go
Raw Permalink Normal View History

2026-08-01 22:06:14 +02:00
package api
import (
"net/url"
"strings"
"testing"
"unicode"
)
// FuzzValidateAvatarURL checks validateAvatarURL against the rule its doc
// comment states: avatar must be either empty, or a URL no longer than
// maxAvatarURLLen characters that parses with scheme "https" and a non-empty
// host. This is the prime target for an active-content escape — an accepted
// "avatar" URL that is actually javascript:, data:, or otherwise not a real
// https:// origin would let a client render/execute it wherever avatars are
// displayed.
func FuzzValidateAvatarURL(f *testing.F) {
seeds := []string{
"",
"https://example.com/avatar.png",
"https://example.com",
"http://example.com/avatar.png",
"javascript:alert(1)",
"JavaScript:alert(1)",
"data:text/html,<script>alert(1)</script>",
"data:image/png;base64,iVBORw0KGgo=",
"vbscript:msgbox(1)",
"https://",
"https:///avatar.png",
"https:example.com",
" https://example.com/avatar.png",
"https://example.com/avatar.png ",
"//example.com/avatar.png",
"file:///etc/passwd",
"https://user:pass@example.com/avatar.png",
"https://例え.com/avatar.png",
strings.Repeat("a", 600),
"https://" + strings.Repeat("a", 600) + ".com/x.png",
"https:// evil.com",
"ht!tp://bad url",
"https:\t//example.com",
}
for _, s := range seeds {
f.Add(s)
}
f.Fuzz(func(t *testing.T, avatar string) {
err := validateAvatarURL(avatar)
if err != nil {
return
}
if avatar == "" {
return
}
if len(avatar) > maxAvatarURLLen {
t.Fatalf("validateAvatarURL(%q) = nil, but length %d exceeds maxAvatarURLLen %d", avatar, len(avatar), maxAvatarURLLen)
}
parsed, perr := url.Parse(avatar)
if perr != nil {
t.Fatalf("validateAvatarURL(%q) = nil, but url.Parse fails: %v", avatar, perr)
}
if parsed.Scheme != "https" {
t.Fatalf("validateAvatarURL(%q) = nil, but parsed scheme is %q, not https (active-content/off-scheme escape)", avatar, parsed.Scheme)
}
if parsed.Host == "" {
t.Fatalf("validateAvatarURL(%q) = nil, but parsed host is empty", avatar)
}
})
}
// FuzzValidateDisplayName checks validateDisplayName against the rule its
// doc comment states: no control characters and no invisible (Cf) formatting
// characters, since a display name renders wherever a username does and a
// bidi override or control character there is a spoofing vector.
func FuzzValidateDisplayName(f *testing.F) {
seeds := []string{
"",
"Normal Name",
"emoji😀name",
"日本語",
"name\x00null",
"name\ttab",
"name\nnewline",
"zero\u200bwidth", // ZERO WIDTH SPACE
"bidi\u202eoverride",
"\u202ereversed\u202c",
strings.Repeat("a", 1000),
"\u200bname\u200b",
"\u202eevil\u202c",
}
for _, s := range seeds {
f.Add(s)
}
f.Fuzz(func(t *testing.T, name string) {
err := validateDisplayName(name)
if err != nil {
return
}
for _, r := range name {
if unicode.IsControl(r) {
t.Fatalf("validateDisplayName(%q) = nil, but contains control rune %q", name, r)
}
if unicode.In(r, unicode.Cf) {
t.Fatalf("validateDisplayName(%q) = nil, but contains invisible (Cf) rune %q", name, r)
}
}
})
}