security: login rate limiting, fix health endpoint, harden error handling

- Fix health endpoint returning null time (use time.Now() instead of context value)
- Add LoginRateLimiter: 5 attempts per 15min window, 5min block on brute-force
- Apply rate limiting to login/register endpoints
- Move reset-password to authenticated routes (was unauthenticated — critical vuln)
- Limit response body sizes on proxy/external API calls (prevent memory exhaustion)
- Sanitize error messages in agent chat endpoint (don't leak internal errors)
- Record login success/failure for rate limiting tracking
This commit is contained in:
shinchan-zhai
2026-03-23 09:43:20 +08:00
parent 84758e2942
commit f28f0e340c
6 changed files with 160 additions and 12 deletions

View File

@@ -66,7 +66,7 @@ func (b *Brain) scanNews(seen map[string]bool) {
resp, err := b.http.Get("https://min-api.cryptocompare.com/data/v2/news/?lang=EN&sortOrder=latest")
if err != nil { return }
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) // 1MB limit
if err != nil { return }
var result struct {
@@ -138,7 +138,7 @@ func (b *Brain) sendBrief(hour int) {
for _, sym := range []string{"BTCUSDT", "ETHUSDT"} {
resp, err := b.http.Get(fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", sym))
if err != nil { continue }
body, readErr := io.ReadAll(resp.Body)
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) // 64KB limit
resp.Body.Close()
if readErr != nil { continue }
var t map[string]string

View File

@@ -287,7 +287,7 @@ func fetchStockQuote(code string) (*StockQuote, error) {
if err != nil { return nil, err }
defer resp.Body.Close()
reader := transform.NewReader(resp.Body, simplifiedchinese.GBK.NewDecoder())
reader := transform.NewReader(io.LimitReader(resp.Body, 256*1024), simplifiedchinese.GBK.NewDecoder())
body, err := io.ReadAll(reader)
if err != nil { return nil, err }

View File

@@ -65,7 +65,8 @@ func (w *WebHandler) HandleChat(rw http.ResponseWriter, r *http.Request) {
resp, err := w.agent.HandleMessage(ctx, req.UserID, msg)
if err != nil {
writeJSON(rw, 500, map[string]string{"error": err.Error()})
w.logger.Error("agent HandleMessage failed", "error", err, "user_id", req.UserID)
writeJSON(rw, 500, map[string]string{"error": "Failed to process message. Please try again."})
return
}
writeJSON(rw, 200, map[string]string{"response": resp})
@@ -107,13 +108,14 @@ func proxyBinance(rw http.ResponseWriter, url string) {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)
if err != nil {
writeJSON(rw, 502, map[string]string{"error": err.Error()})
writeJSON(rw, 502, map[string]string{"error": "upstream request failed"})
return
}
defer resp.Body.Close()
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set("Access-Control-Allow-Origin", "*")
io.Copy(rw, resp.Body)
// Limit response body to 2MB to prevent memory exhaustion
io.Copy(rw, io.LimitReader(resp.Body, 2*1024*1024))
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {