diff --git a/common/batch/blob.go b/common/batch/blob.go index 9ecdfd27b..74d977341 100644 --- a/common/batch/blob.go +++ b/common/batch/blob.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "fmt" "io" + "math" "morph-l2/common/codec/zstd" @@ -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) diff --git a/node/derivation/config.go b/node/derivation/config.go index fb010bab3..a3f194ba8 100644 --- a/node/derivation/config.go +++ b/node/derivation/config.go @@ -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) { diff --git a/node/derivation/config_test.go b/node/derivation/config_test.go index 8701196a5..4148c0c96 100644 --- a/node/derivation/config_test.go +++ b/node/derivation/config_test.go @@ -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 @@ -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, } { diff --git a/token-price-oracle/client/cex_feed.go b/token-price-oracle/client/cex_feed.go index a2102d946..4848ec43b 100644 --- a/token-price-oracle/client/cex_feed.go +++ b/token-price-oracle/client/cex_feed.go @@ -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. @@ -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)) } diff --git a/token-price-oracle/client/cex_feed_test.go b/token-price-oracle/client/cex_feed_test.go index f70028d81..ad41a9c33 100644 --- a/token-price-oracle/client/cex_feed_test.go +++ b/token-price-oracle/client/cex_feed_test.go @@ -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) + } +} + func TestParseFixedStablecoinPrice(t *testing.T) { price, err := parseFixedStablecoinPrice("$1.0") if err != nil {