fix(gameserver): percent-encode ServerDB URI query values (#41) - #109
Merged
Merged
Conversation
RequestRegistration built the legacy ServerDB URI with
snprintf("%s?discord_id=%s&password=%s", ...), so a password containing
'&', '=', '#', '%', '+' or whitespace rewrote the query: '&guilds=999'
injected a parameter, '#' cut the rest of the query off as a fragment
(ixwebsocket drops it), '+' decoded to a space in Go's ParseQuery, and
values were silently truncated at a fixed 1024-byte buffer.
ServerDbUri::BuildLegacyUri / BuildTokenRouteUri now build both URIs.
Every value goes through libcurl's curl_easy_escape (RFC 3986: all bytes
but ALPHA DIGIT - . _ ~ become %XX). guilds/regions are encoded per
element with literal commas, because the server splits the decoded
value on ','. Values the server accepts (session_ws.go discordIDPattern,
regionPattern, guildPattern) are all unreserved, so the wire bytes of a
working config are unchanged. A base that already has a query gets '&'.
The fixed buffer and its four C-style casts are gone. The legacy debug
line logs password presence, never the value.
test_serverdb_uri (11 tests, wired into test-auth-unit) pins the exact
encoding and parses the result back with curl_url + split + unescape.
Mutation check: making EncodeQueryValue return its input (the pre-fix
behaviour) failed 7 of the first 10 tests and the added injection test,
which parsed as {password:"a", guilds:"999"} and lost the real guilds
to a fragment.
Co-Authored-By: nevr-runtime <agents@sprock.io>
compat/ws_bridge.cpp appended nevr_discord_id and nevr_password to the
bridge's remote URL raw ("discordid=" + id + "&password=" + password) for
the config and login connections, on clients and servers. It hits the
same Nakama handler as the ServerDB URI fixed in f138193
(session_ws.go:173-174 discordid/discord_id, :181 password), so after
that commit the binary sent two different passwords for one config value.
The call site now uses ServerDbUri::BuildBridgeCredentialUri, which runs
both values through the same EncodeQueryValue. Both-or-neither and the
"discordid" key are unchanged; the key literal is what the Bearer
selection at ws_bridge.cpp detects. If encoding fails (allocation), the
connection proceeds without URL credentials and logs at Error instead of
sending an unencoded secret.
Tests: four more in test_serverdb_uri (15 total) - exact encoding, a
hostile-value round trip, both-or-neither, and a check that the bridge
and ServerDB paths deliver identical password bytes. Mutation check:
restoring the old concatenation inside the builder fails three of them;
the old path decoded "a+b%41;c&d#e" as "a+bA;c" and left "#e" as a
fragment. A new just-verify sensor fails on any raw "&password="/"+=
cfgPassword"/"[?&]password=%s" in either file and requires both call
sites to use the encoder; falsified against 323352b (hits
gameserver.cpp:1433, ws_bridge.cpp:1002-1003).
COMPATIBILITY RISK (owner decision, not addressed here): Nakama sets an
account's stored password from this URL parameter on the first login
that supplies one (evr_pipeline_login.go:472-475, LinkEmail with
params.authPassword). An account whose password was first set through
the old raw URL and contains '+', '%', '&', '#' or ';' has the mangled
form stored ('+' -> space, %XX decoded, truncated at '&'/'#'; a ';'
drops the parameter entirely in Go's parseQuery). After this change
the client sends the real password, which will not match.
Co-Authored-By: nevr-runtime <agents@sprock.io>
…erdb-uri Resolve justfile test-target list conflict from #113 landing on main: keep both test_serverdb_uri (this branch) and test_websocket_client_auth (main) in both test-auth-unit lists. Co-Authored-By: nevr-runtime <agents@sprock.io>
thesprockee
added a commit
that referenced
this pull request
Oct 6, 2026
…d-registry-violation Resolve src/runtime/CMakeLists.txt list conflicts from #109 landing on main: keep both main_thread_handoff and serverdb_uri in the source/header lists, SKIP_PRECOMPILE_HEADERS list, and the test_serverdb_uri target. Co-Authored-By: nevr-runtime <agents@sprock.io>
1 task done
thesprockee
added a commit
that referenced
this pull request
Oct 6, 2026
gameserver.cpp:1310 cited docs/guides/token-auth-migration.md directly, but 1d75593 deleted that file during the 2026-07-26 doc cleanup. Point the comment at the historical sha (verified against git history, same pattern already used at the nearby line 1501 citation added by #41/ PR #109) and note where the as-built facts now live. Co-Authored-By: nevr-runtime <agents@sprock.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
?discord_id=%s&password=%s-style query strings via snprintf/string concatenation with no escaping.a&guilds=999#tailinjects its own guilds parameter and truncates the real one into the URL fragment (which ixwebsocket drops). Reproduced exactly against both sites with a hostile mutant value.COMPATIBILITY RISK — needs your decision, not addressed in this PR
Nakama sets an account's stored password from this URL parameter on the FIRST login that supplies one (evr_pipeline_login.go:472-475, only when the profile has no password set yet). Before this PR, both paths sent the password unencoded. Of the special characters, only
+, a valid%XXescape, and&/#truncation that still leaves 8+ bytes can produce a MANGLED value actually stored server-side (+becomes a space,%XXis decoded, everything after&/#is cut off). A bare;does NOT produce a stored mismatch — Go'sparseQuerydrops the whole pair, so the password was never set via this path at all (confirmed by independent re-review); same for an invalid%escape, or truncation short enough that nakama's own LinkEmail minimum-length check rejects it outright — those accounts never got a password stored this way and this PR lets them set one for the first time. Nakama also keeps only the first 32 bytes of the decoded password (session_ws.go:181), so mangling past byte 32 never caused a mismatch either. After this PR the client sends the correct, fully-encoded password, which won't match a genuinely-mangled stored value from the+/%XX/&/#cases above — those accounts would fail to authenticate. Passwords using only letters/digits/-._~ are unaffected. Can't tell how many accounts are affected without the production database.Test plan
Found, not fixed here
Not done
🤖 Generated with Claude Code