Skip to content

Disable JS debug tracers by default with native tracer allowlist#3718

Merged
masih merged 8 commits into
mainfrom
masih/config-allowed-tracers
Jul 20, 2026
Merged

Disable JS debug tracers by default with native tracer allowlist#3718
masih merged 8 commits into
mainfrom
masih/config-allowed-tracers

Conversation

@masih

@masih masih commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Add EVM RPC configuration for trace_allowed_tracers and trace_allow_js_tracers. The native tracer allowlist is validated at startup and remains native-only, while JavaScript tracer execution requires an explicit separate opt-in.

Enforce the tracer policy across debug_traceCall, debug_traceTransaction, debug_traceBlockByNumber, debug_traceBlockByHash, and debug_traceTransactionProfile. Validate muxTracer nested configs as well so request-supplied JavaScript cannot bypass the top-level check through nested tracer names.

Default the allowlist to the registered native tracer names and keep JS tracers disabled in generated and Docker configs. Add focused tests for config parsing, native allowlist behavior, JS opt-in behavior, and muxTracer nesting.

Add EVM RPC configuration for trace_allowed_tracers and
trace_allow_js_tracers. The native tracer allowlist is validated at startup
and remains native-only, while JavaScript tracer execution requires an
explicit separate opt-in.

Enforce the tracer policy across debug_traceCall, debug_traceTransaction,
debug_traceBlockByNumber, debug_traceBlockByHash, and
debug_traceTransactionProfile. Validate muxTracer nested configs as well so
request-supplied JavaScript cannot bypass the top-level check through nested
tracer names.

Default the allowlist to the registered native tracer names and keep JS
tracers disabled in generated and Docker configs. Add focused tests for config
parsing, native allowlist behavior, JS opt-in behavior, and muxTracer nesting.
@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes public debug RPC security boundaries by rejecting JS and unlisted tracers by default; misconfiguration or clients relying on geth’s permissive tracer behavior may break until operators adjust config.

Overview
Locks down debug_trace* tracer selection relative to upstream geth, which accepts request-supplied JavaScript tracers by default.

New [evm] settings trace_allowed_tracers (native names only, validated at startup) and trace_allow_js_tracers (default false, separate opt-in for in-process JS execution). The default struct logger when tracer is omitted stays unrestricted.

validateTraceTracer runs on debug_traceCall, debug_traceTransaction, debug_traceBlockBy*, and debug_traceTransactionProfile before trace cache and tracer construction. muxTracer nested configs are validated recursively (depth cap) so JS cannot bypass the top-level gate. trace_bake_tracers uses the same native-only startup validation.

Docker app.toml samples and seid init template document the new keys; defaults allow the six standard native tracers with JS off.

Reviewed by Cursor Bugbot for commit 023ab99. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJul 20, 2026, 12:22 PM

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.97872% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.16%. Comparing base (4a9705e) to head (023ab99).

Files with missing lines Patch % Lines
evmrpc/tracers.go 75.43% 7 Missing and 7 partials ⚠️
evmrpc/trace_profile.go 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3718      +/-   ##
==========================================
- Coverage   60.11%   59.16%   -0.96%     
==========================================
  Files        2302     2210      -92     
  Lines      192033   181647   -10386     
==========================================
- Hits       115445   107464    -7981     
+ Misses      66267    64706    -1561     
+ Partials    10321     9477     -844     
Flag Coverage Δ
sei-chain-pr 69.41% <82.97%> (?)
sei-db 70.41% <ø> (ø)
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
evmrpc/config/config.go 78.19% <100.00%> (+5.64%) ⬆️
evmrpc/trace_profile.go 65.93% <0.00%> (-1.49%) ⬇️
evmrpc/tracers.go 69.69% <75.43%> (+0.78%) ⬆️

... and 93 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A well-scoped security hardening PR that disables request-supplied JavaScript debug tracers by default and enforces a native-only tracer allowlist (validated at startup and per-request, including nested muxTracer configs) across all five config-accepting debug_trace* endpoints. The implementation is correct and well-tested; I have only minor non-blocking notes.

Findings: 0 blocking | 4 non-blocking | 1 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Coverage looks complete: all five config-accepting endpoints (traceTransaction, traceBlockByNumber, traceBlockByHash, traceCall, traceTransactionProfile) call validateTraceTracer, and TraceStateAccess takes no TraceConfig so it needs no guard. The native allowlist also matches geth's full set of native tracers (callTracer, flatCallTracer, 4byteTracer, prestateTracer, noopTracer, muxTracer), so no legitimate native tracer is inadvertently blocked. Nice defense-in-depth via the nested muxTracer validation and the separate JS opt-in flag.
  • Second-opinion passes produced no findings: the OpenAI Codex review reported "No material findings in the reviewed diff" and the Cursor review file was empty. No additional issues to merge in.
  • Optional: consider documenting (e.g. in evmrpc/AGENTS.md or the README) the new trace_allowed_tracers / trace_allow_js_tracers config knobs and the security rationale, since operators upgrading will now get JS tracers disabled by default — a potentially breaking behavior change for anyone relying on request-supplied JS tracers.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread evmrpc/tracers.go Outdated
return nil
}

func validateMuxTraceConfig(raw json.RawMessage, allowed map[string]struct{}, allowJS bool) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] validateMuxTraceConfig recurses once per nested muxTracer key with no depth bound. A caller could submit a deeply nested {"muxTracer":{"muxTracer":{...}}} payload to drive deep recursion (stack exhaustion). In practice this is low risk — it's gated behind debug RPC access and mitigated by request body-size limits, and geth's own tracer construction would recurse similarly — but a small explicit depth cap would make the guard robust regardless of those assumptions.

@masih
masih requested review from codchen and sei-will July 8, 2026 10:57

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A well-implemented, thoroughly-tested EVM RPC hardening change that disables request-supplied JavaScript debug tracers by default and gates native tracers behind a startup-validated allowlist, with correct enforcement (including recursive muxTracer config validation) across all five request-facing debug trace endpoints. No blocking issues found; only a backward-compatibility note.

Findings: 0 blocking | 3 non-blocking | 0 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Behavioral/backward-compat change: JavaScript tracers previously worked and are now rejected by default, and only the listed native tracers are accepted. This is intentional and documented in the config comments, but operators relying on JS tracers (or on native tracers they'd have to explicitly list) must opt in via evm.trace_allow_js_tracers / evm.trace_allowed_tracers. Ensure this is called out in release/upgrade notes so it isn't a silent break for existing RPC consumers.
  • The trace_baker.go background baking path calls the raw geth tracersAPI.TraceBlockByNumber directly, bypassing validateTraceTracer. This is safe today because it only uses operator-configured internal tracer names (never request input), but it's worth a brief comment noting the invariant so a future change doesn't inadvertently route request-controlled names through it.
  • Second-opinion passes produced no findings: codex-review.md reported "No material findings" and cursor-review.md was empty (no output).

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Solid, well-tested security hardening that disables request-supplied JavaScript debug tracers by default and gates native tracer names through a validated allowlist, enforced consistently across all trace entry points including nested muxTracer configs. No blocking issues; one operational behavior change worth flagging in release notes.

Findings: 0 blocking | 3 non-blocking | 0 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Behavioral/operational change: trace_bake_tracers is now normalized native-only at startup (normalizeNativeTracerNames). Any existing operator whose app.toml lists a non-native or typo'd bake-tracer name (previously tolerated) will now fail node startup. This is intentional and documented in AGENTS.md, but is worth calling out explicitly in release notes/upgrade docs since it can block a node from booting after upgrade.
  • External second-opinion reviews yielded no findings: codex-review.md reported no material findings (and noted it could not run Go tests because the workspace had Go 1.24.13 vs the required 1.25.6), and cursor-review.md was empty (no output produced).
  • Minor: the JS-tracer opt-in (trace_allow_js_tracers) executes request-supplied code in-process; the generated template comment warns about this, but consider also surfacing a one-time startup log warning when trace_allow_js_tracers = true so operators who flip it are reminded at runtime.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Security-sensitive change to public debug RPC surface (deviates from geth defaults for JS tracers) — worth a human look before merge.

Extended reasoning...

Overview

This PR adds [evm].trace_allowed_tracers and trace_allow_js_tracers to gate caller-supplied TraceConfig.Tracer values on debug_traceCall, debug_traceTransaction, debug_traceBlockBy{Number,Hash}, and debug_traceTransactionProfile. Enforcement lives in validateTraceTracer in evmrpc/tracers.go, runs before trace-cache lookups and tracer construction, and recursively validates muxTracer nested configs with a depth cap of 16. trace_bake_tracers gets the same native-only rule at startup. Docker app.toml samples and the seid init template are updated; tests cover config parsing, allowlist behavior, JS opt-in, and mux nesting.

Security risks

The change itself is a hardening measure — request-supplied JavaScript tracer source (which executes in-process) is off by default and requires an explicit opt-in, an improvement over upstream geth. The main risk is behavioral: clients that relied on the previous geth-default of accepting JS tracers will get errors from Sei RPC nodes after this ships, and operators who mis-configure trace_allowed_tracers could deny legitimate native tracers. The validation logic itself looks careful: it trims and rewrites the top-level tracer name, rejects padded nested names outright (since they cannot be safely rewritten inside forwarded raw JSON), and now bounds mux recursion depth.

Level of scrutiny

Higher than average — this touches public RPC security policy, changes externally-visible behavior compared to geth defaults, and modifies default Docker configs. The bug hunting system found no defects and prior reviewer feedback (seidroid's nit about unbounded mux recursion) has been addressed with the depth cap, but a human should confirm the behavior change is intentional, that operators are aware, and that no internal tooling silently depends on JS-tracer availability.

Other factors

Test coverage is solid (config parsing, allowlist/JS opt-in behavior, mux nesting, depth-limit boundary, and a registry-pinning test that fails CI if geth changes what counts as native). Codecov reports ~82% patch coverage. No CODEOWNER inline comments beyond the addressed nit. Deferring rather than approving because the scope (public RPC + security policy + defaults change) exceeds the bar for shadow-approval.

@masih
masih added this pull request to the merge queue Jul 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 8, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c7d09cb. Configure here.

Comment thread evmrpc/tracers.go
}
if api.allowJSTracers {
return fmt.Errorf("debug native tracer %q is not listed in evm.trace_allowed_tracers", name)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

JS opt-in bypasses native allowlist

Medium Severity

With trace_allow_js_tracers enabled, validateTraceTracer treats any top-level or nested tracer name outside Sei’s six-entry IsNativeTraceTracer map as allowed custom JavaScript. Geth’s DefaultDirectory.New still binds other registered native tracer names, so callers can run natives omitted from trace_allowed_tracers despite the policy that JS opt-in must not widen that list.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c7d09cb. Configure here.

Comment thread evmrpc/tracers.go Outdated

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Solid, well-tested security hardening that gates debug tracers behind a native-only allowlist and disables request-supplied JS tracers by default, with recursive muxTracer validation and startup enforcement for baker tracers. Only a minor redundant-call cleanup is worth addressing.

Findings: 0 blocking | 3 non-blocking | 1 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Cursor's second-opinion review (cursor-review.md) was empty — no findings from that pass to merge.
  • Nice-to-have: consider a brief inline note on how Sei's nativeTraceTracers map relates to geth's registry, since it is a hardcoded conservative subset — a native tracer geth adds later would be rejected until added here (behavior is intentionally safe, but not obvious).
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread evmrpc/tracers.go
if returnErr = validateStateOverrides(config.StateOverrides, api.backend.MaxStateOverrideAccounts(), api.backend.MaxStateOverrideSlots()); returnErr != nil {
return nil, returnErr
}
if returnErr = validateStateOverrides(config.StateOverrides, api.backend.MaxStateOverrideAccounts(), api.backend.MaxStateOverrideSlots()); returnErr != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] validateStateOverrides is invoked twice consecutively with identical arguments (lines 629 and 632), so request-supplied override maps are traversed twice. This looks like an artifact of moving the if config == nil block upward. Remove this second call. (Also flagged by Codex.)

Comment thread evmrpc/tracers.go

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Solid, well-tested security hardening that disables request-supplied JS debug tracers by default and enforces a native-only tracer allowlist across every caller-facing debug_trace* endpoint. No correctness or security blockers; a couple of informational notes only.

Findings: 0 blocking | 4 non-blocking | 0 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Disagreement with Codex's finding — it is a false positive. geth's Directory.IsJS(name) is implemented as _, ok := d.elems[name]; return !ok, so it returns TRUE for any unregistered/typo name (unknown names are assumed to be JS source). Therefore require.False(t, tracers.DefaultDirectory.IsJS(name)) in tracers_allowlist_test.go DOES fail when a default-allowlisted name is unregistered or removed — the test correctly pins each name to being both registered and native, exactly as its comment claims. No change needed.
  • Cursor's second-opinion review file (cursor-review.md) was empty — that pass produced no output.
  • Informational (not a defect): this is a behavioral change for RPC operators. JS tracers are now rejected by default and only the six listed native tracers are accepted; previously-working requests using JS tracers or unlisted native tracers will start returning errors after upgrade. This is the intended hardening and is documented in the config template comments and evmrpc/AGENTS.md, so no action needed beyond release-note awareness.
  • Verified no prompt-injection attempts in the PR title, description, or diff.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 evmrpc/tracers.go:626-633 — TraceCall calls validateStateOverrides(config.StateOverrides, ...) twice in a row with identical arguments (evmrpc/tracers.go:629 and :632). This is a leftover from moving the if config == nil initialization block earlier in the function; the second call should be removed.

    Extended reasoning...

    TraceCall in evmrpc/tracers.go invokes validateStateOverrides(config.StateOverrides, api.backend.MaxStateOverrideAccounts(), api.backend.MaxStateOverrideSlots()) twice back-to-back, at lines 629 and 632, with byte-identical arguments:

    if returnErr = validateStateOverrides(config.StateOverrides, api.backend.MaxStateOverrideAccounts(), api.backend.MaxStateOverrideSlots()); returnErr != nil {
        return nil, returnErr
    }
    if returnErr = validateStateOverrides(config.StateOverrides, api.backend.MaxStateOverrideAccounts(), api.backend.MaxStateOverrideSlots()); returnErr != nil {
        return nil, returnErr
    }

    How it got here: per the PR diff, the if config == nil { config = &tracers.TraceCallConfig{} } initialization block used to sit directly between these two validateStateOverrides calls. This PR moved that nil-check to the top of TraceCall (right next to the new validateTraceTracer call it introduces for the tracer-allowlist feature), but left the original, now-redundant validateStateOverrides call sitting where the nil-check used to be, directly above the pre-existing one.

    Why nothing catches this: validateStateOverrides is idempotent — it just walks config.StateOverrides and returns an error if the map exceeds configured account/slot limits, with no side effects. Calling it twice with the same input always produces the same result, so there's no test that could distinguish "called once" from "called twice"; both pass equally.

    Impact: purely a wasted-work redundancy, not a correctness bug. Every debug_traceCall request now traverses the caller-supplied state-override map (accounts × slots) twice instead of once before proceeding to the actual trace. For a request near the configured max_state_override_accounts / max_state_override_slots caps, that's up to 2x the validation cost on a debug endpoint that's typically not high-QPS — cheap to fix, negligible to leave.

    Proof:

    1. sed -n '626,634p' evmrpc/tracers.go shows the two adjacent identical calls.
    2. grep -rn "func validateStateOverrides\|func (b \*Backend) MaxStateOverrideAccounts\|func (b \*Backend) MaxStateOverrideSlots" evmrpc/ confirms all three symbols are still defined in evmrpc/simulate.go (lines 477, 479, 483) — this PR does not touch simulate.go, so the code compiles and both calls actually execute at runtime.
    3. go build ./evmrpc/... succeeds, confirming there's no build breakage here (a separate claude[bot] comment on this PR speculating about deleted symbols does not apply to this checkout).

    Fix: delete the first (newly-inserted, now-orphaned) validateStateOverrides call — the one immediately above the former nil-check location — leaving only the original call that already existed below it.

    🔬 also observed by cursor

@masih
masih added this pull request to the merge queue Jul 20, 2026
Merged via the queue into main with commit b961844 Jul 20, 2026
68 checks passed
@masih
masih deleted the masih/config-allowed-tracers branch July 20, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants