aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/server.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 15:16:44 -0400
committerhistoria <[not public]>2026-09-18 15:16:44 -0400
commit7eab582d8a8ee2af1a6baae7baf0696c7005de2e (patch)
tree902bbc8c6e39c9f62f779ca23267714ec420b9f5 /internal/admin/server.go
parent153acb7f644d755c45533b7c5311e3ffa47e8973 (diff)
downloadthehouseoficarus-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.go50
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 != "" {