fix(config): reject auth-source tags that contain a colon
A third-party player's canonical UUID is UUIDv3 over tag+":"+nativeID, and the native id is whatever the source answers. Tags were only checked for being non-empty and unique, so both "guild" and "guild:eu" could be configured. The "guild" root could then answer hasJoined with id "eu:X" and receive exactly the UUID of "guild:eu"'s player X, along with their playerdata, permissions and account links. Real native ids are 32 hex digits, so only the shorter tag's source can do this, and only when the operator has configured such a pair; when they have, it is a full impersonation. Reject a ':' in a tag at load. With colon-free tags the join is unambiguous: two different (tag, id) pairs can no longer produce the same input, since equal inputs force equal tags and duplicate tags are already refused. The tag is deliberately not narrowed any further. It is a permanent UUID namespace, and forcing an operator to rename a tag that has no colon would move every one of its players to a new UUID. The hash input and the native id are left exactly as they were, so no existing player's UUID changes. Load and LoadNano share validateAuthSources; the new test runs both against the guild / guild:eu pair and fails on the previous config.go.
This commit is contained in:
2 files changed
+45
-3
No files matched your search
@@ -275,6 +275,39 @@ url = "https://b.example.net/hasJoined"
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadRejectsColonInAuthSourceTag pins the separator guard. The UUID of a third-party
|
||||
// player is derived from tag+":"+nativeID, and the native id is chosen by the source, so with
|
||||
// "guild" and "guild:eu" both configured the "guild" root could answer id "eu:X" and receive
|
||||
// the UUID of "guild:eu"'s player X. Both loaders share the check, so both are exercised.
|
||||
func TestLoadRejectsColonInAuthSourceTag(t *testing.T) {
|
||||
const sources = `
|
||||
[[auth_source]]
|
||||
tag = "guild"
|
||||
prefix = "GD"
|
||||
url = "https://a.example.net/hasJoined"
|
||||
[[auth_source]]
|
||||
tag = "guild:eu"
|
||||
prefix = "GE"
|
||||
url = "https://b.example.net/hasJoined"
|
||||
`
|
||||
_, errFull := config.Load(writeTOML(t, `
|
||||
[server]
|
||||
root_domain = "mc.example.net"
|
||||
[database]
|
||||
url = "postgres://felis@db/felis"
|
||||
`+sources))
|
||||
_, errNano := config.LoadNano(writeTOML(t, sources))
|
||||
for loader, err := range map[string]error{"Load": errFull, "LoadNano": errNano} {
|
||||
if err == nil {
|
||||
t.Errorf("%s accepted a tag containing ':'", loader)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(err.Error(), `"guild:eu"`) || !strings.Contains(err.Error(), "':'") {
|
||||
t.Errorf("%s: error should name the tag and the ':' rule, got: %v", loader, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadRejectsSchemelessAuthSourceURL pins the silently-dead-source guard: a URL with no
|
||||
// http(s):// scheme makes http.NewRequest fail, so the source never validates any login yet
|
||||
// felis-api boots green. Reject at load with the scheme contract spelled out. An empty tag
|
||||
|
||||
Reference in new issue
Block a user