aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/api_entity_path_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 17:48:26 -0400
committerhistoria <[not public]>2026-09-18 17:48:26 -0400
commit8fe9e3c3fef0049df82281a62afe35dda516dfe2 (patch)
treedd269f2c0710f334e19f00a191260ebc46284a90 /internal/admin/api_entity_path_test.go
parent194164a77eb4f2479b94c19e6f0e1278ab608781 (diff)
downloadthehouseoficarus-8fe9e3c3fef0049df82281a62afe35dda516dfe2.tar.gz
fix: close combat ghost, admin traversal, login throttle, journal double-pay, and quality/economy gaps
Diffstat (limited to 'internal/admin/api_entity_path_test.go')
-rw-r--r--internal/admin/api_entity_path_test.go49
1 files changed, 49 insertions, 0 deletions
diff --git a/internal/admin/api_entity_path_test.go b/internal/admin/api_entity_path_test.go
index 27ecbe8..75fed7a 100644
--- a/internal/admin/api_entity_path_test.go
+++ b/internal/admin/api_entity_path_test.go
@@ -62,6 +62,55 @@ func TestEntityAPICannotEscapeData(t *testing.T) {
}
}
+// TestTechModuleCreateRejectsTraversalID: POST /api/techs and /api/modules
+// once joined an unvalidated id into a filesystem path, allowing an
+// arbitrary write outside the data directory (and a destructive undo).
+func TestTechModuleCreateRejectsTraversalID(t *testing.T) {
+ root := t.TempDir()
+ dir := filepath.Join(root, "data")
+ for _, sub := range []string{"techs", "modules", "players/accounts"} {
+ if err := os.MkdirAll(filepath.Join(dir, sub), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ }
+ accounts := player.NewAccountStore(dir)
+ if err := accounts.SaveAccount(&player.Account{Name: "auditor", Admin: true}); err != nil {
+ t.Fatal(err)
+ }
+ s, err := NewServer(config.Default(), false, accounts, nil, nil, nil, nil, dir,
+ nil, nil, nil, nil, filepath.Join(dir, "config.yaml"),
+ func(fn func()) bool { fn(); return true })
+ if err != nil {
+ t.Fatal(err)
+ }
+ cookieRec := httptest.NewRecorder()
+ s.setAuthCookie(cookieRec, "auditor")
+ cookie := cookieRec.Result().Cookies()[0]
+
+ post := func(path, body string) *httptest.ResponseRecorder {
+ req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
+ req.AddCookie(cookie)
+ rec := httptest.NewRecorder()
+ s.ServeHTTP(rec, req)
+ return rec
+ }
+
+ for _, tc := range []struct{ path, body string }{
+ {"/api/techs", `{"id":"../../outside_tech","name":"x"}`},
+ {"/api/modules", `{"id":"../../outside_mod","name":"x"}`},
+ } {
+ if rec := post(tc.path, tc.body); !strings.Contains(rec.Body.String(), "invalid id") {
+ t.Fatalf("%s traversal id not rejected: status %d body %s", tc.path, rec.Code, rec.Body.String())
+ }
+ }
+ if _, err := os.Stat(filepath.Join(root, "outside_tech.yaml")); !os.IsNotExist(err) {
+ t.Fatal("tech create escaped the data directory")
+ }
+ if _, err := os.Stat(filepath.Join(root, "outside_mod.yaml")); !os.IsNotExist(err) {
+ t.Fatal("module create escaped the data directory")
+ }
+}
+
// TestEntityRenameRejectsDuplicateIDInOtherDirectory: entity IDs are global
// across the type's subdirectory tree (the runtime path indexes resolve by
// filename anywhere under data/<subdir>), so renaming into an ID that