aboutsummaryrefslogtreecommitdiff
path: root/internal/game/hardening_login_validation_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 05:31:09 -0400
committerhistoria <[not public]>2026-09-18 05:31:09 -0400
commit153acb7f644d755c45533b7c5311e3ffa47e8973 (patch)
tree7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/game/hardening_login_validation_test.go
parent87d08f0151e643951769e323995f1c5ed89ee200 (diff)
downloadthehouseoficarus-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.go58
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)
+ }
+}