Security review: canceler RPC hardening findings (mirrors ust1-window sweep) #114
Labels
No labels
agent:implement
agent:ready
backend
bug
cannot-reproduce
confirmed
desktop
docs
documentation
duplicate
enhancement
feature
frontend
good first issue
help wanted
high-risk
in-review
invalid
mobile
needs-triage
P0-critical
P1-high
P2-medium
P3-low
qa
QA
question
ready
report
responsive
security
security-escalate
smart-contract
solana
tablet
test-pass
ux
wallet-issue
wallet:keplr
wallet:metamask
wallet:station
wallet:walletconnect
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-bridge-monorepo#114
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
@PlasticDigits parallel security check on canceler following the ust1-window 40-category sweep. Per your guidance, filing as single summary issue rather than parent + sub-issues since findings are all RPC-related and medium-severity.
Current launch-blocking status: none — you confirmed cl8y xchain activation is fine at this severity level given current asset / rate-limit scope.
Findings
EVM-01: Sequential RPC fallback, not multi-provider cross-check — Medium
canceler/src/evm_client.rsusesmultichain_rs::run_with_evm_rpc_url_fallbackwhich tries primary, falls through to fallbacks on error. First responsive RPC wins. No consensus check across providers.Recommendation: mirror the ust1-window fix — query first 2 URLs in parallel, require 0.01% tolerance agreement, use 3rd URL as tiebreaker. Pattern already proven in
oracle-service8c9fafb.EVM-02: Polls up to
latestwith no confirmation depth — Mediumcanceler/src/watcher.rs:781pollscurrent_block(latest) as the poll end, without subtracting confirmation depth. Reorgs could cause the canceler to act on approvals that get rolled back.Canceler has a reactive 'Chain reset detected — resetting to lookback window' handler (line 772) that clears caches after-the-fact, but no proactive confirmation-depth subtraction.
Recommendation: subtract configurable
EVM_CONFIRMATION_BLOCKS(default ~15 on BSC, ~20 on opBNB) from the poll end. Operator already hasfinality_blocksfield — canceler should follow same pattern.EVM-03: No canonical bridge address allowlist — Low-Med
EVM_BRIDGE_ADDRESSaccepted from env as any hex string. Wrong bridge address (typo, wrong-environment copy) would be silently used.Lower severity than ust1-window EVM-03 because bridge-address changes are governance events (hard to slip past) and mismatch would surface quickly via failed cancels. Still worth a canonical-address startup check for production environments.
EVM-08: HTTPS validation warns but doesn't block — Low-Med
canceler/src/config.rs::validate_rpc_urlacceptshttp://with atracing::warn!("...use https:// in production")rather than erroring. Operator also uses this viamultichain_rs::validate_rpc_url.Easy to miss a warn log; a single misconfigured http:// URL would be MITM-able for the canceler's EVM reads.
Recommendation: add
DEV_ALLOW_HTTP=1env flag (default off) gating loopback-only http allowance. Matches ust1-window fix pattern (0f5a3e4 era).EVM-10:
/healthreturns unconditionalOK, no staleness alert — Mediumcanceler/src/server.rs:/healthreturnsHealthResponse { status: "healthy", ... }regardless of actual staleness/readyzchecks 'has polled ≥1 block ever' — flips to OK once and stays OK forever/metricsexposeslast_evm_block,last_terra_height(good) but no built-in thresholdExternal Prometheus with alerting rules could monitor this, but the service-local health endpoints don't detect 'stuck canceler' states.
Recommendation: track
last_successful_cancel_ts+last_successful_poll_ts; readiness/health return non-OK if silence exceeds configurable threshold (default ~4-8h depending on expected cancel frequency). Pattern: ust1-windowliveness.rsin 31a8b3a.EVM-14: No
eth_chainIdverification — Low-MedcancelerreadsEVM_CHAIN_IDfrom env (u64) but never callseth_chainIdon the configured RPC to verify. Wrong-chain RPC (opBNB pointed at BSC account, mainnet pointed at testnet, etc) would be silently used.Recommendation: startup
verify_all_bsc_rpc_urlsequivalent — calleth_chainIdon every configured URL, error if mismatch againstEVM_CHAIN_ID. Pattern: ust1-windowbsc.rsin 673ac74.EVM-18: ✅ PASS (no finding)
Config,EvmConfig,TerraConfig,SolanaConfig,DatabaseConfigall have manualfmt::Debugimpls that redact secrets (evm_private_key,terra_mnemonic,private_key, database URL). Explicit note: "NOTE: Debug is manually implemented to redact sensitive fields — Do NOT re-add#[derive(Debug)]."Already covered by canceler security review C1. Clean.
Cross-references
PlasticDigits/ust1-window#5(all 9 findings closed as of today)b3b8937,8c9fafb,0f5a3e4,c42ca85,31a8b3a,673ac74,7cd0c45,7bcc285in ust1-windowOperator counterpart
Operator has a very similar profile — same findings mostly apply except EVM-02 (operator already has
finality_blocks, defaults to 1 which is weak but addresses the mechanic). Filing as a separate issue for component-level tracking.mentioned in issue #115
mentioned in commit
8efb987c25Implemented on
main(commit8efb987, branchfix/gl-114-115-rpc-hardeningmerged).EVM-01 / quorum:
multichain-rsnow resolveseth_blockNumberacross up to 3 endpoints with 0.01% tolerance and a configurable quorum (EVM_RPC_AGREEMENT_QUORUM, defaultmin(2, url_count)). With multiple URLs,EVM_RPC_AGREEMENT_QUORUM=1is rejected unlessEVM_RPC_SINGLE_ENDPOINT_READS=1pins reads to the first URL (covers single-reliable-RPC chains like opBNB while blocking silent failover to a different truth). Canceler uses the same policy for enumeration, events,getThisChainId, andcan_cancel; cancel tx submission tries the consensus endpoint first then fallbacks.EVM-02:
EVM_CONFIRMATION_BLOCKS(chain-aware default: BSC 15, opBNB 20, else 12) subtracted from the EVM head for approval polling.EVM-03: Optional
EVM_CANONICAL_BRIDGE_ADDRESSESallowlist.EVM-08: Shared
validate_rpc_url— remotehttp://blocked unlessDEV_ALLOW_HTTP=1or loopback host.EVM-10:
/healthJSON + status 503 when idle exceedsCANCELER_HEALTH_MAX_IDLE_SECS(default 8h) afterCANCELER_HEALTH_STARTUP_GRACE_SECS(default 5m);/readyzconsiders staleness. Activity timestamp updated each successfulpoll_approvalscycle.EVM-14: Startup
verify_evm_jsonrpc_chain_idson all primary + multi-EVM RPC URLs.@Brouie please verify in your environment (especially multi-RPC quorum vs single-URL +
EVM_RPC_SINGLE_ENDPOINT_READS).@PlasticDigits verified on
8efb987.Build + test baseline on branch:
Findings:
EvmRpcReadPolicy+evm_consensus_latest_blockparallel quorum with 0.01% tolerance. Defaultmin(2, url_count), explicit error on quorum=1 with multiple URLs. Unit tests in rpc_fallback.rs cover the policy.EVM_CONFIRMATION_BLOCKSfirst-class, chain-aware defaults (BSC=15, opBNB=20, default=12),saturating_subin watcher.rs at both event-scan sites.check_bridge_allowlist()at startup, case-insensitive, opt-in viaEVM_CANONICAL_BRIDGE_ADDRESSES. Validates primary + all multi-EVM bridge addresses.validate_rpc_url(multichain-rs) rejects remote http withoutDEV_ALLOW_HTTP=1, allows loopback, rejects file:// and ftp://. Full unit coverage.verify_evm_jsonrpc_chain_idsat startup, fails fast with index + expected/actual.Minor cross-service note (not blocker): canceler defaults opBNB confirmation=20, operator defaults opBNB finality=12. Both safe, just asymmetric — flagging for your awareness.
Closing as verified.