Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion token-price-oracle/client/cex_feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const (
okxTickerPath = "/api/v5/market/ticker"
)

// maxResponseBodyBytes caps how much of an HTTP price response is read into memory.
// Ticker and Hermes latest-price payloads are a few KB at most; the cap only exists
// so a compromised or misbehaving endpoint cannot stream an unbounded body and
// exhaust memory.
const maxResponseBodyBytes = 1 << 20 // 1 MiB

type cexPriceFetcher func(ctx context.Context, httpClient *http.Client, baseURL string, symbol string) (*big.Float, error)

// CEXPriceFeed fetches token prices from a centralized exchange REST API.
Expand Down Expand Up @@ -243,10 +249,13 @@ func getJSONWithHeaders(ctx context.Context, httpClient *http.Client, requestURL
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodyBytes+1))
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if int64(len(body)) > maxResponseBodyBytes {
return nil, fmt.Errorf("response body exceeds %d byte limit", maxResponseBodyBytes)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("HTTP status %d: %s", resp.StatusCode, string(body))
}
Expand Down
32 changes: 32 additions & 0 deletions token-price-oracle/client/cex_feed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,38 @@ func TestFetchOKXPrice(t *testing.T) {
}
}

func TestGetJSONRejectsOversizedBody(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Stream more than the cap so a compromised/misbehaving endpoint cannot
// force an unbounded allocation.
oversized := make([]byte, maxResponseBodyBytes+1)
w.Write(oversized)
}))
defer server.Close()

if _, err := getJSON(context.Background(), server.Client(), server.URL); err == nil {
t.Fatal("getJSON accepted an oversized response body, want error")
}
}

func TestGetJSONAcceptsBodyAtLimit(t *testing.T) {
payload := `{"symbol":"BTCUSDT","price":"64385.12"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(payload))
}))
defer server.Close()

body, err := getJSON(context.Background(), server.Client(), server.URL)
if err != nil {
t.Fatal(err)
}
if string(body) != payload {
t.Fatalf("body = %q, want %q", string(body), payload)
}
}
Comment on lines +118 to +133

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the boundary test send exactly maxResponseBodyBytes bytes.

The current payload is much smaller than the limit. This test does not verify the exact boundary and would pass even if boundary handling were incorrect. Pad the valid JSON with trailing whitespace to reach the configured byte limit.

Proposed fix
-	payload := `{"symbol":"BTCUSDT","price":"64385.12"}`
+	payload := `{"symbol":"BTCUSDT","price":"64385.12"}`
+	payload += strings.Repeat(" ", maxResponseBodyBytes-len(payload))

Add "strings" to the import block if needed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestGetJSONAcceptsBodyAtLimit(t *testing.T) {
payload := `{"symbol":"BTCUSDT","price":"64385.12"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(payload))
}))
defer server.Close()
body, err := getJSON(context.Background(), server.Client(), server.URL)
if err != nil {
t.Fatal(err)
}
if string(body) != payload {
t.Fatalf("body = %q, want %q", string(body), payload)
}
}
func TestGetJSONAcceptsBodyAtLimit(t *testing.T) {
payload := `{"symbol":"BTCUSDT","price":"64385.12"}`
payload += strings.Repeat(" ", maxResponseBodyBytes-len(payload))
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(payload))
}))
defer server.Close()
body, err := getJSON(context.Background(), server.Client(), server.URL)
if err != nil {
t.Fatal(err)
}
if string(body) != payload {
t.Fatalf("body = %q, want %q", string(body), payload)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@token-price-oracle/client/cex_feed_test.go` around lines 118 - 133, Update
TestGetJSONAcceptsBodyAtLimit so the valid JSON payload is padded with trailing
whitespace using strings.Repeat until its length is exactly
maxResponseBodyBytes; add the strings import if needed, while preserving the
existing request and body-equality assertions.


func TestParseFixedStablecoinPrice(t *testing.T) {
price, err := parseFixedStablecoinPrice("$1.0")
if err != nil {
Expand Down