From 95e0995c7fd42f76964f819204ef11982d9691f2 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Fri, 22 May 2026 07:25:08 -0700 Subject: [PATCH] Forex/camp review hardening: bound CoinGecko body, escape URL params, fail-open camp on DB error - forex_crypto.go: cap the CoinGecko response with io.LimitReader(64KiB); build the simple/price query via url.Values instead of bare Sprintf. - dnd_expedition_camp.go: a combat-session lookup error now fails open (allow camp) + logs a warning, instead of blocking standard camp with a misleading empty 'not cleared' rejection. --- internal/plugin/dnd_expedition_camp.go | 9 ++++++++- internal/plugin/forex_crypto.go | 11 ++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/internal/plugin/dnd_expedition_camp.go b/internal/plugin/dnd_expedition_camp.go index e6cbbf8..0b8f876 100644 --- a/internal/plugin/dnd_expedition_camp.go +++ b/internal/plugin/dnd_expedition_camp.go @@ -2,6 +2,7 @@ package plugin import ( "fmt" + "log/slog" "math/rand/v2" "strings" "time" @@ -369,7 +370,13 @@ func campLocationCheck(exp *Expedition) (cleared bool, problem string) { encID := encounterIDForRoom(run.CurrentRoom) sess, err := getCombatSessionForEncounter(run.RunID, encID) if err != nil { - return false, "" + // Can't read the encounter's combat state. Fail open like the + // run-lookup path above rather than blocking standard camp with an + // empty (misleading "not cleared") rejection — a DB hiccup shouldn't + // strand a player who only wants to rest. + slog.Warn("camp: combat session lookup failed; allowing camp", + "run", run.RunID, "encounter", encID, "err", err) + return true, "" } if sess != nil && sess.Status == CombatStatusActive { return false, "You can't camp mid-fight — finish the encounter first." diff --git a/internal/plugin/forex_crypto.go b/internal/plugin/forex_crypto.go index e71c094..29b3595 100644 --- a/internal/plugin/forex_crypto.go +++ b/internal/plugin/forex_crypto.go @@ -4,7 +4,9 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" + "net/url" "strings" "sync" "time" @@ -82,8 +84,11 @@ func (p *ForexPlugin) cgSpotUSD(sym string) (float64, error) { ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) defer cancel() - url := fmt.Sprintf("%s/simple/price?ids=%s&vs_currencies=usd", coinGeckoBaseURL, info.CoinGeckoID) - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + q := url.Values{} + q.Set("ids", info.CoinGeckoID) + q.Set("vs_currencies", "usd") + reqURL := coinGeckoBaseURL + "/simple/price?" + q.Encode() + req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) if err != nil { return 0, err } @@ -99,7 +104,7 @@ func (p *ForexPlugin) cgSpotUSD(sym string) (float64, error) { } var data map[string]map[string]float64 - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + if err := json.NewDecoder(io.LimitReader(resp.Body, 64<<10)).Decode(&data); err != nil { return 0, fmt.Errorf("coingecko decode error: %w", err) } entry, ok := data[info.CoinGeckoID]