security: add safe.ReadAllLimited — bound all HTTP response body reads to 10MB

- Created safe/io.go with ReadAllLimited helper (default 10MB limit)
- Replaced 62 unbounded io.ReadAll(resp.Body) calls across 32 files
- Covers all exchange traders (Hyperliquid, Bybit, Binance, OKX, Aster,
  KuCoin, Gate, Bitget, Lighter, Indodax), providers (CoinAnk, Alpaca,
  TwelveData), MCP client/x402, market data, wallet, telegram, kernel
- Prevents OOM from malicious/buggy exchange API responses
- Previously fixed: brain.go, sentinel.go already had manual LimitReader
This commit is contained in:
shinchan-zhai
2026-03-23 10:55:03 +08:00
parent 1d0d6f7afd
commit 982ee668c9
33 changed files with 120 additions and 89 deletions

View File

@@ -371,7 +371,7 @@ func (client *Client) Call(systemPrompt, userPrompt string) (string, error) {
defer resp.Body.Close()
// Step 6: Read response body (fixed logic)
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
@@ -510,7 +510,7 @@ func (client *Client) callWithRequestFull(req *Request) (*LLMResponse, error) {
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
@@ -548,7 +548,7 @@ func (client *Client) callWithRequest(req *Request) (string, error) {
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
@@ -721,7 +721,7 @@ func (client *Client) CallWithRequestStream(req *Request, onChunk func(string))
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
body, _ := safe.ReadAllLimited(resp.Body)
return "", fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}

View File

@@ -126,7 +126,7 @@ func DoX402Request(
paymentHeader = resp.Header.Get("X-Payment-Required")
}
if paymentHeader == "" {
body, _ := io.ReadAll(resp.Body)
body, _ := safe.ReadAllLimited(resp.Body)
return nil, fmt.Errorf("received 402 but no Payment-Required header found. Body: %s", string(body))
}
@@ -161,7 +161,7 @@ func DoX402Request(
return nil, fmt.Errorf("failed to send payment retry: %w", err)
}
body2, readErr := io.ReadAll(resp2.Body)
body2, readErr := safe.ReadAllLimited(resp2.Body)
resp2.Body.Close()
if readErr != nil {
return nil, fmt.Errorf("failed to read payment retry response: %w", readErr)
@@ -221,7 +221,7 @@ func DoX402Request(
return nil, fmt.Errorf("%s payment retry failed (status %d): %s", providerTag, lastStatus, string(lastBody))
}
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
@@ -261,7 +261,7 @@ func DoX402RequestStream(
if resp.StatusCode == http.StatusOK {
return resp, nil
}
body, _ := io.ReadAll(resp.Body)
body, _ := safe.ReadAllLimited(resp.Body)
resp.Body.Close()
return nil, fmt.Errorf("%s API error (status %d): %s", providerTag, resp.StatusCode, string(body))
}
@@ -272,7 +272,7 @@ func DoX402RequestStream(
paymentHeader = resp.Header.Get("X-Payment-Required")
}
if paymentHeader == "" {
body, _ := io.ReadAll(resp.Body)
body, _ := safe.ReadAllLimited(resp.Body)
resp.Body.Close()
return nil, fmt.Errorf("received 402 but no Payment-Required header found. Body: %s", string(body))
}
@@ -320,7 +320,7 @@ func DoX402RequestStream(
}
// Non-200: read body for error handling / re-sign
body2, readErr := io.ReadAll(resp2.Body)
body2, readErr := safe.ReadAllLimited(resp2.Body)
resp2.Body.Close()
if readErr != nil {
return nil, fmt.Errorf("failed to read payment retry response: %w", readErr)