aboutsummaryrefslogtreecommitdiff
path: root/internal/game/hardening_login_validation_test.go
blob: fa60497983abe2c7e56ec11375232183af556e7b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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)
	}
}