mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* fix(admin): accept same-origin first-run setup requests A freshly generated config.yaml leaves allowed_origins commented out, so the list is empty. The setup handler's CSRF guard assumed "no Origin header means same-origin", but browsers send Origin on same-origin POSTs too — Chrome and Edge always, Firefox since 70. The admin panel's own setup call is one of those POSTs, so every new install hit "cross-origin setup request blocked" and could never create an owner account. The guard now accepts a request whose Origin names the same host:port as the request's own Host header, falling back to the allowlist otherwise. That is what the original comment intended. CSRF protection is unaffected: a cross-site attacker cannot set Origin, the browser does, and a foreign origin still needs an explicit allowlist entry. Scheme is not compared. Nothing in this server derives the external scheme (no r.TLS or X-Forwarded-Proto handling exists anywhere), so a scheme check would reject legitimate requests behind a TLS-terminating proxy. Tests: isSameOrigin table covering port/host/suffix/schemeless/opaque-origin cases, plus two handler-level tests pinning both halves — same-origin succeeds against an empty allowlist, a foreign origin still 403s and creates no user. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(identity): implement identity keypair caching and error handling --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
181 lines
4.3 KiB
Go
181 lines
4.3 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
})
|
|
}
|
|
}
|