Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 13 additions & 1 deletion common/batch/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/binary"
"fmt"
"io"
"math"

"morph-l2/common/codec/zstd"

Expand Down Expand Up @@ -242,11 +243,22 @@ func extractInnerTxFullBytes(firstByte byte, reader io.Reader) ([]byte, error) {
return nil, fmt.Errorf("declared tx size %d exceeds remaining %d bytes", size, lr.Len())
}

// Guard the reconstructed length against uint32 overflow before allocating.
// size is a uint32, so 1+sizeByteLen+size can exceed MaxUint32 and wrap to a
// tiny value, leaving fullTxBytes shorter than the copies below and
// panicking. The remaining-bytes guard above keeps this unreachable today,
// but computing in uint64 and rejecting overflow keeps it safe if the size
// type or the upstream bounds ever change.
fullLen := 1 + uint64(sizeByteLen) + uint64(size)
if fullLen > math.MaxUint32 {
return nil, fmt.Errorf("declared tx size %d overflows uint32 length prefix", size)
}

txRaw := make([]byte, size)
if err := binary.Read(reader, binary.BigEndian, txRaw); err != nil {
return nil, err
}
fullTxBytes := make([]byte, 1+uint32(sizeByteLen)+size)
fullTxBytes := make([]byte, fullLen)
copy(fullTxBytes[:1], []byte{firstByte})
copy(fullTxBytes[1:1+sizeByteLen], sizeByte)
copy(fullTxBytes[1+sizeByteLen:], txRaw)
Expand Down
7 changes: 7 additions & 0 deletions node/derivation/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ func (c *Config) SetCliContext(ctx *cli.Context) error {

if c.VerifyMode == VerifyModeLayer1 {
c.MetricsPort = ctx.GlobalUint64(flags.MetricsPort.Name)
// Reject an out-of-range port up front: MetricsPort is a uint64, so a
// misconfigured value would only surface later as an invalid listen
// address whose ListenAndServe fails silently in the background. The
// default (26660) is always in range, so a valid config never trips this.
if c.MetricsPort == 0 || c.MetricsPort > 65535 {
return fmt.Errorf("--%s must be in 1..65535, got %d", flags.MetricsPort.Name, c.MetricsPort)
}
}

if ctx.GlobalIsSet(flags.DerivationReorgCheckDepth.Name) {
Expand Down
27 changes: 27 additions & 0 deletions node/derivation/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,32 @@ func TestVerifyMode_RejectsUnknown(t *testing.T) {
}
}

func TestMetricsPort_AcceptsDefaultInLayer1(t *testing.T) {
cfg := DefaultConfig()
if err := cfg.SetCliContext(newVerifyModeTestContext(t, map[string]string{
flags.DerivationVerifyMode.Name: VerifyModeLayer1,
})); err != nil {
t.Fatalf("layer1 with default metrics-port rejected: %v", err)
}
if cfg.MetricsPort != 26660 {
t.Fatalf("default metrics-port = %d, want 26660", cfg.MetricsPort)
}
}

func TestMetricsPort_RejectsOutOfRange(t *testing.T) {
cfg := DefaultConfig()
err := cfg.SetCliContext(newVerifyModeTestContext(t, map[string]string{
flags.DerivationVerifyMode.Name: VerifyModeLayer1,
flags.MetricsPort.Name: "70000",
}))
if err == nil {
t.Fatal("out-of-range metrics-port accepted, want error")
}
if !strings.Contains(err.Error(), flags.MetricsPort.Name) {
t.Fatalf("error should mention %q; got: %v", flags.MetricsPort.Name, err)
}
}

func TestBeaconRpcList(t *testing.T) {
for _, tc := range []struct {
name string
Expand Down Expand Up @@ -126,6 +152,7 @@ func newVerifyModeTestContext(t *testing.T, values map[string]string) *cli.Conte
for _, f := range []cli.Flag{
flags.LegacyValidatorMode,
flags.DerivationVerifyMode,
flags.MetricsPort,
flags.L1BeaconAddr,
flags.L2EngineJWTSecret,
} {
Expand Down
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
Loading