| name | rpc-surface-audit |
| description | L1 trigger - audits JSON-RPC and Engine API surfaces for authentication bypass, rate limiting, subscription buffer overflows, and method-specific DoS. |
Injectable Skill: RPC Surface Audit
L1 trigger: L1_PATTERN=true AND (rpc/ OR jsonrpc OR engine_api OR eth/api OR websocket OR ipc detected in recon subsystem map)
Inject Into: depth-network-surface
Language: Go and Rust
Finding prefix: [RPC-N]
Status: v0.1 draft, Round 4 exemplars pending
Orchestrator Decomposition Guide
- Section 1: depth-network-surface (attack surface)
- Section 2: depth-network-surface (auth + rate limit)
- Section 3: depth-state-trace (subscription state)
- Section 4: depth-edge-case (method boundaries)
When This Skill Activates
Recon identifies an RPC subsystem. RPC is the most publicly exposed attack surface on any L1 node — typically unauthenticated (HTTP JSON-RPC, WebSocket) or semi-authenticated (Engine API with JWT). Even a Medium-severity bug here often upgrades to High because of permissionless reachability (see severity-matrix.md modifier).
1. Attack Surface Enumeration
Enumerate all RPC entry points via LSP workspace/symbol filtered by known method-registration patterns:
| Transport | Registration pattern | Example |
|---|
| HTTP JSON-RPC | rpc.Register, httpServer.Handle, #[method(name=...)] | eth_*, debug_*, admin_* |
| WebSocket | Same as HTTP + subscription handlers | eth_subscribe |
| IPC (unix socket) | Often same as HTTP | Geth admin namespace |
| Engine API | JWT-authenticated | engine_newPayloadV*, engine_forkchoiceUpdatedV* |
| Prometheus metrics | /metrics endpoint | often bound to all interfaces |
| PPROF profiling | /debug/pprof/ (Go) | should NEVER be public |
Write the enumeration to scratchpad/rpc_surface.md.
2. Authentication and Namespace Gating
2a. Default exposure
- What namespaces are enabled by default on HTTP? (Geth:
eth, net, web3. Not admin, debug, personal.)
- Is the default bind address localhost or 0.0.0.0?
- Is authentication required for any namespace?
2b. Dangerous namespaces
admin_* — should never be exposed on HTTP; check the config validation
debug_* — can expose internal state; check default
personal_* — key management; deprecated in Geth but still present in forks
engine_* — JWT-authenticated; check the JWT secret handling
miner_* / txpool_* — varies by client
2c. CORS and origin checks
- Is the WebSocket origin-checked? (Yes in Geth; verify in forks)
- Are CORS headers tight? Default should be same-origin
2d. JWT handling (Engine API specifically)
- JWT secret rotation: what happens on secret change mid-operation?
- JWT
iat claim: is clock drift tolerated? Excessive tolerance is a replay window
- JWT algorithm whitelisting: must be HS256 only; reject
alg=none and key-confusion attacks
Tag: [RPC-AUTH:{issue}]
3. Rate Limiting and Resource Quotas
3a. Per-request cost tracking
Some RPC methods are cheap (eth_blockNumber), others are expensive (eth_getLogs with wide range, debug_traceTransaction). Is there a cost model that bounds work per request?
Check:
- For each expensive method, is there a result size limit? E.g.,
eth_getLogs with max_logs_per_request
- For each historical query, is there a block range limit? E.g.,
eth_getLogs with fromBlock → toBlock span
- For each trace method, is there a trace depth / trace time limit?
3b. Per-client rate limit
- Is there rate limiting by IP, by API key, by connection?
- What's the default burst + sustained rate?
3c. Total concurrent request cap
- Maximum simultaneous HTTP requests
- Maximum simultaneous WebSocket connections
- Maximum subscriptions per WebSocket connection
Tag: [RPC-RATE:{scope}:{limit-or-unbounded}]
3d. Outbound HTTP client timeout audit
The skill historically focused on incoming RPC. Audit OUTBOUND HTTP clients used by the node for peer fetches and inter-node API calls — they are a symmetric DoS vector.
Check:
- For each
reqwest::Client::builder(), awc::Client::builder(), hyper::Client::builder(), and http::Client::new() site, verify .timeout(...) AND .connect_timeout(...) are set explicitly. The library defaults are NO timeout.
- For each
tokio::time::timeout wrapper around an outbound call, verify the timeout bound is a constant (not derived from peer-controlled input).
- For Go:
http.Client{} literal (zero value) has NO timeout — flag every site that constructs http.Client without setting Timeout. Also check Transport.DialContext for connect-timeout.
- Fail mode: an unresponsive remote peer can hang the calling task forever, blocking critical bootstrap, sync, or peer-handshake routines. Combined with peer-list flooding, an attacker can stall every honest node's startup.
Tag: [RPC-CLIENT-NO-TIMEOUT:{file}:{line}]
3e. JSON number precision for u64 fields
JavaScript clients (and many other dynamic languages) cannot represent integers above 2^53 - 1 precisely. L1 nodes that serialize u64 fields as JSON numbers (not strings) will silently corrupt block heights, balances, gas values, and timestamps when consumed by JS clients.
Check:
- For each
serde::Serialize impl on a struct exposed via RPC, find every u64 / u128 / i64 / i128 field
- Verify the serializer either uses
#[serde(with = "as_string")] or a similar string-coercion attribute, OR that the field's max value provably fits in 53 bits
- For Go: check
json.Marshal of uint64 fields and *hexutil.Uint64 wrappers (the latter is correct, the former is not)
Tag: [RPC-JSON-PRECISION:{type}.{field}]
3f. Success Return vs Side-Effect Completion
For each RPC handler, peer HTTP endpoint, and outbound API client wrapper that returns Ok(()), HTTP 2xx, a success JSON body, or increments peer score, verify that the underlying side effect completed first.
Check:
- A handler must not return success when validation failed, delivery failed, or the requested state transition was skipped.
- Outbound calls must propagate transport errors, non-2xx status, timeout, parse failure, and body-delivery failure before any success return or peer-score reward.
- For each
Ok(()), StatusCode::OK, HttpResponse::Ok, or equivalent success path, trace the immediately preceding awaited call and ensure it is guarded by ?, explicit error mapping, or a checked status branch.
- For Go, check
return nil paths after Do(req), Write, Encode, channel send, or peer delivery; success must be after the side effect, not before it.
Tag: [RPC-OK-BEFORE-EFFECT:{file}:{line}]
4. Subscription Buffer Overflows
WebSocket subscriptions are a classic DoS vector: slow consumer, fast producer.
Check:
- For each
eth_subscribe type (newHeads, logs, newPendingTransactions, syncing), what is the outbound buffer depth per subscriber?
- When the buffer fills, does the server drop messages, kill the subscription, or block the producer?
- Can a slow consumer cause back-pressure on the chain head update path? (Very bad — affects consensus.)
Tag: [RPC-SUB:{type}:{buffer-policy}]
5. Per-Method Deep Checks
5a. eth_getLogs
- Block range validation
- Bloom filter efficiency on wide ranges
- Empty-log edge case
- Topic filter malformed input
5b. debug_traceTransaction / debug_traceBlock
- Opcode count bound
- Memory bound per trace
- Timeout per trace
- Tracer code injection (for custom tracers)
5c. eth_call / eth_estimateGas
- Gas cap per call
- State override safety (can caller manipulate state?)
- Call depth limit
5d. eth_getProof
- Proof size for a deeply nested account
- Empty account edge case
5e. engine_newPayload (Engine API)
- Payload validation: any panic path?
- State diff computation: unbounded?
- Concurrent newPayload races: handled?
6. Panic Surfaces
An RPC method that panics crashes the node. The Go standard library recovers in HTTP handlers by default, but custom dispatch may not.
Check:
- Is there a top-level
recover / panic catcher?
- For each handler, trace panic paths:
panic!(), .unwrap() on user input, integer division, out-of-bounds access
- For Rust:
.unwrap() and .expect() on decoded user input are panic-on-attack
Tag: [RPC-PANIC:{method}:{path}]
7. Boundary conditions
| State | Test | Expected | Observed |
|---|
| Empty params | method with empty params | spec-defined (often error) | |
| Huge params | 10 MB of JSON in request | rejected before parsing | |
| Deeply nested JSON | 1000-level nesting | rejected (stack overflow guard) | |
| Unknown method | method not registered | error, not crash | |
| Concurrent duplicate sub | same subscription twice | spec-defined | |
| getLogs 10M blocks | wide range | rejected | |
8. Output schema
- Layer: rpc
- Bug class: auth-bypass / rate-limit-missing / subscription-dos / method-dos / panic-surface
- Preferred evidence tags:
[FUZZ-PASS] > [LSP-TRACE] > [CODE-TRACE]
- Severity baseline: Medium-High. Panic-reachable-from-unauthenticated = High or Critical (permissionless node crash)
9. Known bug exemplars (v0.2 — Round 4 verified)
-
Geth GraphQL DoS (CVE-2023-42319, GHSA-v9jh-j8px-98vq) — with --http --graphql enabled, a crafted GraphQL query consumed memory and hung the daemon. Fixed in v1.13.5. GHSA-v9jh-j8px-98vq. Skill catch point: Section 3a — every user-controlled query parser needs query depth cap, memory cap per request, total time cap, allocated-bytes cap.
-
Geth CVE-2025-24883 p2p handshake invalid EC point — RLPx handshake accepted an all-zero secp256k1 public key without validating it lies on the curve. ECDH math with the bogus point produced undefined behavior and DoS. DailyCVE. Methodology nuance: the p2p handshake is part of the RPC attack surface, not a separate domain. Any attacker-supplied curve point must be validated on-curve AND in prime-order subgroup before any math.
-
Geth snap/1 trie-node nil-deref panic (CVE-2021-41173, GHSA-59hh-656j-3p7v) — trie.TryGetNode returns nil for child of fullnode when path exhausted; caller dereferenced without a nil check → origNode.cache() panic. Any malicious snap peer could kill any node. GHSA-59hh-656j-3p7v. Skill catch point: Section 6 (Panic surfaces) — every p2p message handler must never panic. Each .unwrap(), nil-deref, panic-on-slice-index, uncovered switch is a finding.
-
Engine API JWT replay / time-skew exploitation — the Engine API spec allows ±60s iat window. Implementations that fail to enforce freshness allow JWT reuse across rotation boundaries. execution-apis auth spec. Skill catch point: Section 2d — hard iat window, no clock-jump tolerance, rotation-on-restart, no persistence across process boundaries.
Methodology nuance from Round 4 (handshake is RPC surface)
Add to Section 1: The attack surface enumeration MUST include p2p handshake and pre-auth message handlers, not just JSON-RPC methods. CVE-2025-24883 was classified in the RPC surface because the handshake is the first code that touches untrusted input. Cross-reference with p2p-dos-and-eclipse Section 2f (pre-auth panic check).
10. Fallback if primitives unavailable
- Grep for
"eth_" / "debug_" / "admin_" method names
- Find the dispatch table / method registration
- Read the top-level HTTP handler for auth gate
Cross-references
- Related:
mempool-asymmetric-dos (eth_sendRawTransaction is an insertion), execution-client-hardening (debug methods touch VM)
- Consumed by:
depth-network-surface
- Severity:
docs/l1-mode/severity-matrix.md