The page explained the API, said keys come from an operator, and left the reader there. Someone who reaches the address without a key had no route to getting one, which is where onboarding stopped. `landing.contact` is that route, because the gateway cannot guess who to ask. An address becomes a mailto link, an http(s) URL stays a link, and anything else — a channel, a queue, a person — is shown as text. Other schemes are never linked: this string comes from configuration, configuration is not always written by whoever runs the gateway, and a javascript: href on a page served to every visitor is a short path to a stored XSS. Escaping the text does not make a scheme safe. Unconfigured, it now says something rather than nothing, and the page explains that a key arrives as a one-time link so the reader knows what to expect before it lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
48 lines
1.4 KiB
Go
48 lines
1.4 KiB
Go
package web
|
|
|
|
import (
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// contactLink decides how to render the configured contact.
|
|
//
|
|
// An address becomes a mailto:, an http(s) URL stays a link, and anything else
|
|
// — a channel name, an internal ticket queue, a person — is shown as text.
|
|
//
|
|
// The scheme allowlist is the point. This string comes from configuration, but
|
|
// configuration is not always written by the person who runs the gateway, and a
|
|
// javascript: or data: href on a page served to every visitor is a stored XSS
|
|
// with a very short path to it. Templates escape the text; they do not make a
|
|
// scheme safe.
|
|
func contactLink(contact string) (text, href string) {
|
|
c := strings.TrimSpace(contact)
|
|
if c == "" {
|
|
return "", ""
|
|
}
|
|
|
|
if u, err := url.Parse(c); err == nil {
|
|
switch strings.ToLower(u.Scheme) {
|
|
case "http", "https", "mailto":
|
|
return c, c
|
|
case "":
|
|
// No scheme. A bare address is the common case and worth linking.
|
|
if looksLikeEmail(c) {
|
|
return c, "mailto:" + c
|
|
}
|
|
}
|
|
}
|
|
return c, ""
|
|
}
|
|
|
|
// looksLikeEmail is deliberately crude: it decides whether to add a mailto:,
|
|
// and being wrong means a link that does not open a mail client rather than
|
|
// anything worse.
|
|
func looksLikeEmail(s string) bool {
|
|
at := strings.IndexByte(s, '@')
|
|
if at <= 0 || at == len(s)-1 || strings.ContainsAny(s, " \t/") {
|
|
return false
|
|
}
|
|
return strings.Contains(s[at+1:], ".")
|
|
}
|