diff options
| author | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
| commit | 153acb7f644d755c45533b7c5311e3ffa47e8973 (patch) | |
| tree | 7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/game/hardening_login_validation_test.go | |
| parent | 87d08f0151e643951769e323995f1c5ed89ee200 (diff) | |
| download | thehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz | |
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/game/hardening_login_validation_test.go')
| -rw-r--r-- | internal/game/hardening_login_validation_test.go | 58 |
1 files changed, 58 insertions, 0 deletions
diff --git a/internal/game/hardening_login_validation_test.go b/internal/game/hardening_login_validation_test.go new file mode 100644 index 0000000..fa60497 --- /dev/null +++ b/internal/game/hardening_login_validation_test.go @@ -0,0 +1,58 @@ +package game + +// Regression: the account name typed at the prompt must be validated with +// player.ValidName BEFORE it reaches the filesystem. The name is later stored +// on the session and reused to build account/character paths, so an unvalidated +// "../../" style name must never be joined into a path, even if such a path +// happens to resolve to an existing file. + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "thehouseoficarus/internal/net" +) + +func TestAccountNameValidatedBeforeFilesystemAccess(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + + // A file that "../escape" resolves to once joined under players/accounts/ + // (filepath.Join cleans the traversal). Before the fix this name reached + // FindAccount first and was accepted into the password state. + escapePath := filepath.Join(h.dir, "players", "escape.yaml") + if err := os.WriteFile(escapePath, []byte("name: escape\npassword_hash: x\n"), 0600); err != nil { + t.Fatal(err) + } + + conn := newChanConn() + sess := &net.Session{Conn: conn} + sess.SetState(net.StateAccountName) + + h.g.RunOnOwner(func() { h.g.handleAccountName(sess, "../escape") }) + + if got := sess.State(); got != net.StateAccountName { + t.Fatalf("traversal account name accepted: state = %v, want StateAccountName", got) + } + if sess.Account != nil { + t.Fatalf("rejected name bound to the session: %+v", sess.Account) + } + if out := string(conn.Bytes()); !strings.Contains(out, "name must be") { + t.Fatalf("missing name-validation message; output = %q", out) + } + + // A valid existing name still reaches the password prompt. + conn2 := newChanConn() + valid := &net.Session{Conn: conn2} + valid.SetState(net.StateAccountName) + h.g.RunOnOwner(func() { h.g.handleAccountName(valid, "alice") }) + + if got := valid.State(); got != net.StatePassword { + t.Fatalf("valid account name rejected: state = %v, want StatePassword", got) + } + if valid.Account == nil || valid.Account.Name != "alice" { + t.Fatalf("valid account name not bound: %+v", valid.Account) + } +} |
