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

@@ -1,12 +1,12 @@
package aster
import (
"nofx/safe"
"context"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math"
"math/big"
"net/http"
@@ -100,7 +100,7 @@ func (t *AsterTrader) getPrecision(symbol string) (SymbolPrecision, error) {
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
body, _ := safe.ReadAllLimited(resp.Body)
var info struct {
Symbols []struct {
Symbol string `json:"symbol"`
@@ -392,7 +392,7 @@ func (t *AsterTrader) doRequest(method, endpoint string, params map[string]inter
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
body, _ := safe.ReadAllLimited(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
@@ -418,7 +418,7 @@ func (t *AsterTrader) doRequest(method, endpoint string, params map[string]inter
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
body, _ := safe.ReadAllLimited(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}

View File

@@ -1,10 +1,10 @@
package aster
import (
"nofx/safe"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"nofx/logger"
"nofx/trader/types"
@@ -109,7 +109,7 @@ func (t *AsterTrader) GetMarketPrice(symbol string) (float64, error) {
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
body, _ := safe.ReadAllLimited(resp.Body)
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
@@ -263,7 +263,7 @@ func (t *AsterTrader) GetOrderBook(symbol string, depth int) (bids, asks [][]flo
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
body, _ := safe.ReadAllLimited(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}

View File

@@ -1,13 +1,13 @@
package bitget
import (
"nofx/safe"
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"nofx/logger"
"strconv"
@@ -193,7 +193,7 @@ func (t *BitgetTrader) doRequest(method, path string, body interface{}) ([]byte,
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
respBody, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}

View File

@@ -6,7 +6,6 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"nofx/logger"
"nofx/market"
@@ -79,7 +78,7 @@ func (t *BybitTrader) getTradesViaHTTP(startTime time.Time, limit int) ([]BybitT
}
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)
}

View File

@@ -1,9 +1,9 @@
package bybit
import (
"nofx/safe"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"nofx/logger"
@@ -101,7 +101,7 @@ func (t *BybitTrader) getQtyStep(symbol string) float64 {
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return 1
}

View File

@@ -1,13 +1,13 @@
package bybit
import (
"nofx/safe"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"nofx/trader/types"
"strconv"
@@ -134,7 +134,7 @@ func (t *BybitTrader) getClosedPnLViaHTTP(startTime time.Time, limit int) ([]typ
}
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)
}

View File

@@ -1,10 +1,10 @@
package bybit
import (
"nofx/safe"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"nofx/logger"
"nofx/trader/types"
@@ -696,7 +696,7 @@ func (t *BybitTrader) GetOrderBook(symbol string, depth int) (bids, asks [][]flo
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
body, _ := safe.ReadAllLimited(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}

View File

@@ -1,10 +1,10 @@
package hyperliquid
import (
"nofx/safe"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"nofx/logger"
"nofx/trader/types"
@@ -224,7 +224,7 @@ func (t *HyperliquidTrader) getXYZDexBalance() (accountValue float64, unrealized
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return 0, 0, nil, fmt.Errorf("failed to read response: %w", err)
}
@@ -315,7 +315,7 @@ func (t *HyperliquidTrader) getXyzMarketPrice(coin string) (float64, error) {
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return 0, fmt.Errorf("failed to read response: %w", err)
}

View File

@@ -1,10 +1,10 @@
package hyperliquid
import (
"nofx/safe"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"nofx/logger"
"nofx/trader/types"
@@ -452,7 +452,7 @@ func (t *HyperliquidTrader) cancelXyzOrders(coin 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)
}
@@ -545,7 +545,7 @@ func (t *HyperliquidTrader) cancelXyzOrder(oid int64) 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)
}
@@ -686,7 +686,7 @@ func (t *HyperliquidTrader) placeXyzOrder(coin string, isBuy bool, size float64,
}
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 body: %w", err)
}
@@ -850,7 +850,7 @@ func (t *HyperliquidTrader) placeXyzTriggerOrder(coin string, isBuy bool, size f
}
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 body: %w", err)
}

View File

@@ -1,10 +1,10 @@
package hyperliquid
import (
"nofx/safe"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"nofx/logger"
"strings"
@@ -74,7 +74,7 @@ func (t *HyperliquidTrader) fetchXyzMeta() 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)
}

View File

@@ -1,12 +1,12 @@
package indodax
import (
"nofx/safe"
"crypto/hmac"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"nofx/logger"
@@ -131,7 +131,7 @@ func (t *IndodaxTrader) doPublicRequest(path string) ([]byte, error) {
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
data, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
@@ -168,7 +168,7 @@ func (t *IndodaxTrader) doPrivateRequest(params url.Values) ([]byte, error) {
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
data, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}

View File

@@ -1,13 +1,13 @@
package kucoin
import (
"nofx/safe"
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"nofx/logger"
@@ -127,7 +127,7 @@ func (t *KuCoinTrader) syncServerTime() 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)
}
@@ -232,7 +232,7 @@ func (t *KuCoinTrader) doRequest(method, path string, body interface{}) ([]byte,
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
respBody, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}

View File

@@ -1,9 +1,9 @@
package lighter
import (
"nofx/safe"
"encoding/json"
"fmt"
"io"
"net/http"
"nofx/logger"
"strconv"
@@ -26,7 +26,7 @@ func (t *LighterTraderV2) getFullAccountInfo() (*AccountInfo, error) {
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return nil, err
}
@@ -305,7 +305,7 @@ func (t *LighterTraderV2) GetMarketPrice(symbol string) (float64, error) {
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return 0, err
}
@@ -379,7 +379,7 @@ func (t *LighterTraderV2) GetOrderBook(symbol string, depth int) (bids, asks [][
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return nil, nil, err
}

View File

@@ -1,9 +1,9 @@
package lighter
import (
"nofx/safe"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"nofx/logger"
@@ -120,7 +120,7 @@ func (t *LighterTraderV2) GetOrderStatus(symbol string, orderID string) (map[str
}
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 body: %w", err)
}
@@ -236,7 +236,7 @@ func (t *LighterTraderV2) GetActiveOrders(symbol string) ([]OrderResponse, 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)
}

View File

@@ -1,10 +1,10 @@
package lighter
import (
"nofx/safe"
"context"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"net/url"
@@ -211,7 +211,7 @@ func (t *LighterTraderV2) getAccountByL1Address() (*AccountInfo, error) {
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return nil, err
}
@@ -286,7 +286,7 @@ func (t *LighterTraderV2) getApiKeyFromServer() (string, error) {
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return "", err
}
@@ -484,7 +484,7 @@ func (t *LighterTraderV2) GetTrades(startTime time.Time, limit int) ([]tradertyp
}
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)
}

View File

@@ -1,10 +1,10 @@
package lighter
import (
"nofx/safe"
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"nofx/logger"
@@ -389,7 +389,7 @@ func (t *LighterTraderV2) submitOrder(txType int, txInfo string) (map[string]int
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
respBody, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return nil, err
}
@@ -546,7 +546,7 @@ func (t *LighterTraderV2) fetchMarketList() ([]MarketInfo, 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)
}

View File

@@ -1,6 +1,7 @@
package okx
import (
"nofx/safe"
"bytes"
"crypto/hmac"
"crypto/rand"
@@ -9,7 +10,6 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"nofx/logger"
"strings"
@@ -228,7 +228,7 @@ func (t *OKXTrader) doRequest(method, path string, body interface{}) ([]byte, er
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
respBody, err := safe.ReadAllLimited(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}