diff options
Diffstat (limited to 'internal/admin/api_entity_path_test.go')
| -rw-r--r-- | internal/admin/api_entity_path_test.go | 49 |
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 |
