diff options
| author | historia <[not public]> | 2026-09-18 15:16:44 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 15:16:44 -0400 |
| commit | 7eab582d8a8ee2af1a6baae7baf0696c7005de2e (patch) | |
| tree | 902bbc8c6e39c9f62f779ca23267714ec420b9f5 /internal/admin/server.go | |
| parent | 153acb7f644d755c45533b7c5311e3ffa47e8973 (diff) | |
| download | thehouseoficarus-7eab582d8a8ee2af1a6baae7baf0696c7005de2e.tar.gz | |
fix: harden triggers, economy, casino, world data, and admin/net paths
Diffstat (limited to 'internal/admin/server.go')
| -rw-r--r-- | internal/admin/server.go | 50 |
1 files changed, 48 insertions, 2 deletions
diff --git a/internal/admin/server.go b/internal/admin/server.go index 4a54c78..4ea22d8 100644 --- a/internal/admin/server.go +++ b/internal/admin/server.go @@ -365,6 +365,20 @@ func (s *AdminServer) authMiddleware(next http.Handler) http.Handler { }) } +// UndoStack returns the server's undo history. Two admin transports over the +// same data directory must share one stack: each server otherwise loads and +// rewrites the same history file independently, so records made through one +// are silently dropped by the next write from the other. +func (s *AdminServer) UndoStack() *UndoStack { return s.undoStack } + +// SetUndoStack replaces the server's undo history with a shared stack. Call +// before serving requests. +func (s *AdminServer) SetUndoStack(us *UndoStack) { + if us != nil { + s.undoStack = us + } +} + func (s *AdminServer) checkAuth(r *http.Request) bool { cookie, err := r.Cookie("admin_session") if err != nil { @@ -389,8 +403,16 @@ func (s *AdminServer) checkAuth(r *http.Request) bool { return false } + // Bind the signature to the current password hash as well as the issue + // time, so changing an admin password invalidates every previously issued + // cookie instead of leaving it valid for the full TTL. The admin check + // above already loaded the account; reload it here for the hash. + acc, err := s.accountStore.LoadAccount(account) + if err != nil || !acc.Admin { + return false + } mac := hmac.New(sha256.New, s.cookieSecret) - mac.Write([]byte(account + ":" + parts[1])) + mac.Write([]byte(account + ":" + parts[1] + ":" + acc.PasswordHash)) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(sig), []byte(expected)) } @@ -552,9 +574,15 @@ func clientIP(remoteAddr string) string { } func (s *AdminServer) setAuthCookie(w http.ResponseWriter, account string) { + // Include the current password hash in the MAC so a password change + // revokes outstanding cookies (see checkAuth). + hash := "" + if acc, err := s.accountStore.LoadAccount(account); err == nil { + hash = acc.PasswordHash + } issued := strconv.FormatInt(time.Now().Unix(), 10) mac := hmac.New(sha256.New, s.cookieSecret) - mac.Write([]byte(account + ":" + issued)) + mac.Write([]byte(account + ":" + issued + ":" + hash)) sig := hex.EncodeToString(mac.Sum(nil)) http.SetCookie(w, &http.Cookie{ Name: "admin_session", @@ -607,6 +635,16 @@ func (s *AdminServer) handleLogin(w http.ResponseWriter, r *http.Request) { account := strings.TrimSpace(r.FormValue("account")) password := r.FormValue("password") + // Validate before the name is joined into a filesystem path: LoadAccount + // does no validation, so an unvalidated name is an arbitrary-*.yaml read + // primitive ("account=../../rooms/1"). Every other account-name entry + // point validates first. + if err := player.ValidName(account); err != nil { + limiter.fail(ip) + s.renderPage(w, r, "login.html", map[string]any{"Error": "Invalid account name or password."}) + return + } + acc, err := s.accountStore.LoadAccount(account) if err != nil || !player.CheckPassword(password, acc.PasswordHash) { limiter.fail(ip) @@ -1004,6 +1042,14 @@ func (s *AdminServer) invalidateEntityCacheForPath(path string) { } } +// pathUnderDir reports whether path lies inside dataDir/subdir (and is not the +// directory itself). Used by the raw-rename path to refresh caches that the +// shared invalidateEntityCacheForPath does not own. +func pathUnderDir(dataDir, subdir, path string) bool { + rel, err := filepath.Rel(filepath.Join(dataDir, subdir), path) + return err == nil && rel != "." && !strings.HasPrefix(rel, "..") +} + func changeFiles(change *ChangeDesc) []string { files := []string{change.FilePath} if change.NewFilePath != "" { |
