2026-07-25 14:47:21 +00:00
|
|
|
package admin
|
|
|
|
|
|
|
|
|
|
import "testing"
|
|
|
|
|
|
|
|
|
|
// isSetupOriginAllowed guards the first-run setup endpoint, which creates the
|
|
|
|
|
// server owner. It had no coverage. The safe default matters most: an empty
|
|
|
|
|
// allowlist must deny, not allow.
|
|
|
|
|
|
|
|
|
|
func TestIsSetupOriginAllowed(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
origin string
|
|
|
|
|
allowed []string
|
|
|
|
|
want bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "empty allowlist denies",
|
|
|
|
|
origin: "https://app.example",
|
|
|
|
|
allowed: nil,
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "empty allowlist denies even an empty origin",
|
|
|
|
|
origin: "",
|
|
|
|
|
allowed: nil,
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "exact match allows",
|
|
|
|
|
origin: "https://app.example",
|
|
|
|
|
allowed: []string{"https://app.example"},
|
|
|
|
|
want: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "match is case-insensitive",
|
|
|
|
|
origin: "https://APP.example",
|
|
|
|
|
allowed: []string{"https://app.example"},
|
|
|
|
|
want: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "wildcard allows anything",
|
|
|
|
|
origin: "https://anywhere.example",
|
|
|
|
|
allowed: []string{"*"},
|
|
|
|
|
want: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "wildcard anywhere in the list allows",
|
|
|
|
|
origin: "https://anywhere.example",
|
|
|
|
|
allowed: []string{"https://app.example", "*"},
|
|
|
|
|
want: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "non-matching origin denied",
|
|
|
|
|
origin: "https://evil.example",
|
|
|
|
|
allowed: []string{"https://app.example"},
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "match against any list entry",
|
|
|
|
|
origin: "https://second.example",
|
|
|
|
|
allowed: []string{"https://first.example", "https://second.example"},
|
|
|
|
|
want: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "a suffix of an allowed origin is not a match",
|
|
|
|
|
origin: "https://evil-app.example",
|
|
|
|
|
allowed: []string{"https://app.example"},
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "a prefix of an allowed origin is not a match",
|
|
|
|
|
origin: "https://app.example.evil.test",
|
|
|
|
|
allowed: []string{"https://app.example"},
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "scheme must match too",
|
|
|
|
|
origin: "http://app.example",
|
|
|
|
|
allowed: []string{"https://app.example"},
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
if got := isSetupOriginAllowed(tt.origin, tt.allowed); got != tt.want {
|
|
|
|
|
t.Errorf("isSetupOriginAllowed(%q, %v) = %v, want %v",
|
|
|
|
|
tt.origin, tt.allowed, got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-30 21:50:13 +02:00
|
|
|
|
|
|
|
|
// isSameOrigin is what lets the admin panel's own setup call through on a
|
|
|
|
|
// default config, where allowed_origins is empty. It must not become a hole:
|
|
|
|
|
// only an Origin naming this exact host:port may pass.
|
|
|
|
|
func TestIsSameOrigin(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
origin string
|
|
|
|
|
host string
|
|
|
|
|
want bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "admin panel on the default port is same-origin",
|
|
|
|
|
origin: "https://localhost:8443",
|
|
|
|
|
host: "localhost:8443",
|
|
|
|
|
want: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "host comparison is case-insensitive",
|
|
|
|
|
origin: "https://LocalHost:8443",
|
|
|
|
|
host: "localhost:8443",
|
|
|
|
|
want: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "plain http against a proxied host still matches",
|
|
|
|
|
origin: "http://chat.example",
|
|
|
|
|
host: "chat.example",
|
|
|
|
|
want: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "a different host is not same-origin",
|
|
|
|
|
origin: "https://evil.example",
|
|
|
|
|
host: "localhost:8443",
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "a different port is not same-origin",
|
|
|
|
|
origin: "https://localhost:9999",
|
|
|
|
|
host: "localhost:8443",
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "loopback by IP does not match loopback by name",
|
|
|
|
|
origin: "https://127.0.0.1:8443",
|
|
|
|
|
host: "localhost:8443",
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "a suffix of the host is not same-origin",
|
|
|
|
|
origin: "https://evil-localhost:8443",
|
|
|
|
|
host: "localhost:8443",
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "schemeless origin cannot pass as same-origin",
|
|
|
|
|
origin: "//localhost:8443",
|
|
|
|
|
host: "localhost:8443",
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "opaque origin (sandboxed iframe) is denied",
|
|
|
|
|
origin: "null",
|
|
|
|
|
host: "localhost:8443",
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "empty origin is denied",
|
|
|
|
|
origin: "",
|
|
|
|
|
host: "localhost:8443",
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "empty host denies rather than matching an empty origin host",
|
|
|
|
|
origin: "https://localhost:8443",
|
|
|
|
|
host: "",
|
|
|
|
|
want: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
if got := isSameOrigin(tt.origin, tt.host); got != tt.want {
|
|
|
|
|
t.Errorf("isSameOrigin(%q, %q) = %v, want %v",
|
|
|
|
|
tt.origin, tt.host, got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|