Skip to content

Raw transaction replay-protection middleware can be bypassed with extra positional params #381

Description

@Kewe63

Bug Description

RejectUnprotectedTxsMiddleware is intended to reject pre-EIP-155 / replay-unprotected legacy transactions submitted through public JSON-RPC when allow_unprotected_txs is false. However, the positional parameter parser currently extracts raw transaction bytes with an exact one-element tuple parse:

req.params().parse::<(Bytes,)>().ok().map(|(bytes,)| bytes)

A JSON-RPC request with a valid legacy raw transaction plus an extra positional parameter can make this middleware extraction fail. The middleware then treats the request as unparseable/malformed and forwards it to the inner RPC handler instead of applying Arc's replay-protection rejection.

If the downstream JSON-RPC method ignores the extra positional parameter and consumes the first raw transaction argument, this bypasses Arc's public RPC policy and allows pre-EIP-155 raw transaction submissions despite the default rejection setting.

Affected methods:

  • eth_sendRawTransaction
  • eth_sendRawTransactionSync

Code Evidence

crates/evm-node/src/rpc_middleware.rs:

fn error_if_unprotected_send_raw_tx<'a>(req: &Request<'a>) -> Result<(), ErrorObject<'a>> {
    if !is_raw_transaction_submission(req.method_name()) {
        return Ok(());
    }
    let Some(bytes) = extract_raw_tx_bytes(req) else {
        return Ok(());
    };
    let Ok(envelope) = TxEnvelope::decode_2718_exact(bytes.as_ref()) else {
        return Ok(());
    };
    if envelope.is_replay_protected() {
        return Ok(());
    }
    Err(ErrorObjectOwned::owned::<()>(
        UNPROTECTED_TX_ERROR_CODE,
        UNPROTECTED_TX_ERROR_MSG,
        None,
    ))
}

extract_raw_tx_bytes handles object params and exact one-element positional params, but does not reject malformed positional shapes at the policy boundary:

fn extract_raw_tx_bytes(req: &Request<'_>) -> Option<Bytes> {
    #[derive(serde::Deserialize)]
    struct SendRawTransactionParams {
        bytes: Bytes,
    }
    if req.params().is_object() {
        req.params()
            .parse::<SendRawTransactionParams>()
            .ok()
            .map(|p| p.bytes)
    } else {
        req.params().parse::<(Bytes,)>().ok().map(|(bytes,)| bytes)
    }
}

When the tuple parse fails, error_if_unprotected_send_raw_tx returns Ok(()), which allows the request to continue to the inner service.

Steps to Reproduce

  1. Check out current upstream main:
git clone https://github.com/circlefin/arc-node arc-node-triage
cd arc-node-triage
git checkout de76122a1c4756e747accc27db35ffc8ec8981a8
  1. Add this focused regression test inside the existing #[cfg(test)] mod tests in crates/evm-node/src/rpc_middleware.rs:
#[tokio::test]
async fn poc_extra_positional_param_bypasses_pre_eip155_rejection() {
    let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService);
    let raw_hex = encode_legacy_raw(None);
    let request = send_raw_tx_request_with_params(format!(r#"["{raw_hex}", "ignored"]"#), 999);
    let response = middleware.call(request).await;

    assert!(
        response.as_error_code().is_none(),
        "extra positional parameter makes middleware forward an unprotected raw tx"
    );
    let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap();
    assert_eq!(json["result"], "success");
}
  1. Run the focused test:
RUSTUP_TOOLCHAIN=1.94.0 cargo test -p arc-evm-node poc_extra_positional_param_bypasses_pre_eip155_rejection -- --nocapture

Actual Behavior

A pre-EIP-155 raw transaction with an extra positional parameter is forwarded by the middleware instead of being rejected by Arc's replay-protection policy.

Example request shape:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_sendRawTransaction",
  "params": [
    "0x<valid legacy raw transaction signed without EIP-155 chain_id>",
    "ignored"
  ]
}

Expected Behavior

RejectUnprotectedTxsMiddleware should not silently allow policy-sensitive malformed parameter shapes. It should either:

  • extract and validate the first raw transaction argument consistently with the downstream handler, or
  • reject raw transaction submission calls whose params are not one of the explicitly supported safe shapes.

In all cases, pre-EIP-155 raw transactions should be rejected when allow_unprotected_txs is false, regardless of trailing ignored params.

Impact

This can bypass Arc's default replay-protection policy for public RPC raw transaction submission paths. A public RPC client may submit replay-unprotected legacy transactions by adding an ignored second positional parameter, undermining the node operator's expectation that pre-EIP-155 raw transactions are blocked unless explicitly allowed.

Duplicate Check

Searched public issues and PRs in circlefin/arc-node for:

  • sendRawTransaction extra positional
  • unprotected transaction extra params
  • pre-EIP-155 raw transaction
  • extract_raw_tx_bytes
  • allow_unprotected_txs
  • trailing raw transaction
  • RPC replay-protection

No matching issue or PR for this root cause was found. The visible matches were unrelated docs/consensus items, not this middleware bypass.

Environment

  • Repository: circlefin/arc-node
  • Commit tested: de76122a1c4756e747accc27db35ffc8ec8981a8
  • Package: arc-evm-node
  • Component: crates/evm-node/src/rpc_middleware.rs

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions