For the complete documentation index, see llms.txt. This page is also available as Markdown.

75432 bc insight attacker can force unsafe head adoption through admin postunsafepayload even when admin rpc is not enabled

Submitted on Apr 29th 2026 at 04:33:34 UTC by @p_laksmana for Audit Comp | Base Azul

  • Report ID: #75432

  • Report Type: Blockchain/DLT

  • Report severity: Insight

  • Target: https://github.com/base/base/tree/v0.8.0-rc.28

  • Impacts:

    • Network not being able to confirm new transactions (total network shutdown)

Description

Brief/Intro

In the current Base Azul base-consensus implementation, the consensus RPC CLI exposes a BASE_NODE_RPC_ENABLE_ADMIN / --rpc.enable-admin flag that appears intended to gate the admin RPC namespace. However, in the full base-consensus node service path, the admin RPC module is merged whenever the internal network_admin channel exists. The code never checks RpcBuilder.enable_admin before registering the admin namespace.

As a result, a normal Base Azul full node can expose admin_postUnsafePayload even when the operator did not enable the admin API.

Once reachable, admin_postUnsafePayload accepts an arbitrary BaseExecutionPayloadEnvelope and forwards it into the node's unsafe-block path. The path does not require a sequencer client, does not verify an unsafe-block signer, and does not require the payload to arrive through signed consensus gossip. If the execution layer returns Valid, the production InsertTask imports the payload, runs forkchoice synchronization, and updates the node's unsafe head to the attacker-supplied block.

For example:

  1. Operator runs Base Azul base-consensus node with RPC enabled but without BASE_NODE_RPC_ENABLE_ADMIN=true.

  2. The full node service still passes network_admin: Some(net_admin_rpc) into RpcActor.

  3. RpcActor merges AdminRpc because network_admin is Some.

  4. Attacker reaches the consensus RPC listener and calls admin_postUnsafePayload.

  5. The attacker supplies a valid-looking but attacker-controlled BaseExecutionPayloadEnvelope.

  6. The payload is forwarded to engine_client.send_unsafe_block(payload).

  7. The engine path creates an InsertTask.

  8. If engine_newPayload and engine_forkchoiceUpdated return Valid, the node adopts the attacker-controlled block as its unsafe head.

Vulnerability Details

The CLI defines the admin flag and stores it in RpcArgs.crates/client/cli/src/rpc.rs#L24-L32

The flag is then copied into RpcBuilder.crates/client/cli/src/rpc.rs#L69-L78

RpcBuilder contains the enable_admin field.crates/consensus/rpc/src/config.rs#L7-L16

However, the full node service passes network_admin: Some(net_admin_rpc) to the RPC actor whenever the RPC actor is spawned.crates/consensus/service/src/service/node.rs#L544-L555

Inside RpcActor::start, the admin namespace is registered based only on the presence of network_admin. The code does not check self.config.enable_admin.crates/consensus/service/src/actors/rpc/actor.rs#L119-L123

The admin method accepts the unsafe payload and forwards it to the network actor. The comment explicitly says there is no sequencer guard.crates/consensus/rpc/src/admin.rs#L70-L81

The network actor forwards the admin-posted payload directly to the engine unsafe-block path.crates/consensus/service/src/actors/network/actor.rs#L203-L208

The downstream engine client turns that payload into EngineActorRequest::ProcessUnsafeL2BlockRequest.crates/consensus/service/src/actors/network/engine_client.rs#L30-L39

The engine actor routes that request into the processing queue.crates/consensus/service/src/actors/engine/actor.rs#L153-L155

The engine processor creates a production InsertTask for the received payload.crates/consensus/service/src/actors/engine/engine_request_processor.rs#L585-L594

Inside InsertTask, once engine_newPayload returns a valid or syncing status, the task constructs a new unsafe block reference. When the later forkchoice update is confirmed, this becomes the node's unsafe head.crates/consensus/engine/src/task_queue/tasks/insert/task.rs#L120-L141

SynchronizeTask applies the new sync state when engine_forkchoiceUpdated returns Valid.crates/consensus/engine/src/task_queue/tasks/synchronize/task.rs#L153-L162

Since the admin path bypasses the intended RPC admin gate and does not verify the unsafe-block signer before reaching this engine path, a reachable consensus RPC listener lets the attacker inject an unsafe payload into the same import/canonicalization path used for normal unsafe blocks.

As a result, anyone who can reach the affected Base Azul consensus RPC listener can force that node to adopt an attacker-controlled unsafe head whenever the execution layer accepts the payload as valid.

Impact Details

This issue can cause attacker-controlled unsafe-head adoption and sequencer confirmation failure in Base-native consensus/offchain.

An attacker who can reach the consensus RPC listener can call admin_postUnsafePayload even when BASE_NODE_RPC_ENABLE_ADMIN is disabled. The payload enters the unsafe-block import path without sequencer authorization, unsafe-block signer validation, a TEE/ZK proof, or an on-chain transaction. If the execution layer returns Valid, production InsertTask updates the node's unsafe_head to the attacker-supplied block.

If the injected head is inconsistent with the selected L1 origin, the sequencer cannot start new block builds. Production PayloadBuilder calls reset_engine_forkchoice(), returns None, and never calls start_build_block(), so no new L2 block build begins while the condition is maintained.

As a result, affected nodes may diverge on unsafe head, and a reachable sequencer can suffer confirmation failure. If the same condition is maintained on the live sequencer path or enough consensus-critical nodes, block production and confirmation may fail network-wide, potentially causing the network to be unable to confirm new transactions.

Attack Scenario

1

Attacker identifies a reachable Base Azul consensus RPC listener.

The attacker finds a consensus RPC listener that can be reached.

2

The operator has not enabled BASE_NODE_RPC_ENABLE_ADMIN.

BASE_NODE_RPC_ENABLE_ADMIN is disabled.

3

RpcActor still registers the admin namespace.

Because RpcActor ignores RpcBuilder.enable_admin, the admin namespace is still registered on the full node path.

4

Attacker calls admin_postUnsafePayload.

The attacker invokes the admin method.

5

Attacker supplies a valid-looking BaseExecutionPayloadEnvelope.

The supplied payload is accepted by the RPC method.

6

AdminRpc forwards the payload to NetworkAdminQuery::PostUnsafePayload.

The admin RPC forwards the payload into the network layer.

7

NetworkActor forwards the same payload to engine_client.send_unsafe_block.

The network actor sends the payload onward.

8

The engine path creates and runs InsertTask.

The unsafe payload enters the production import path.

9

If the execution layer accepts the payload and the forkchoice update returns Valid, the node's unsafe head becomes the attacker-supplied block.

The unsafe head is updated to the injected block.

10

On the sequencer path, the next PayloadBuilder::build() reads that attacker-controlled unsafe head.

The sequencer build path consumes the altered unsafe head.

11

If the injected head has an inconsistent L1 origin, PayloadBuilder::get_next_payload_l1_origin() calls reset_engine_forkchoice() and returns None.

The sequencer cannot build on an inconsistent L1 origin.

12

Because build() returns None, the sequencer does not create an UnsealedPayloadHandle.

No payload handle is produced.

13

Without an UnsealedPayloadHandle, the sequencer does not call start_build_block().

Block building does not begin.

14

Without start_build_block(), there is no new execution payload to seal, gossip, or insert as the next unsafe L2 block.

No new block is produced.

15

While the attacker repeatedly maintains this inconsistent injected head, new user transactions cannot be included in newly built L2 blocks by the affected sequencer.

Transaction inclusion is prevented while the condition persists.

16

As a result, the affected sequencer fails to confirm new transactions for as long as the condition is maintained.

The sequencer cannot confirm new transactions.

17

If this condition is maintained on the live sequencer path or enough consensus-critical nodes, block production and confirmation can fail network-wide, causing the network to be unable to confirm new transactions.

Network-wide confirmation failure can occur.

References

Recomendatation

Gate admin RPC registration on RpcBuilder.enable_admin. network_admin: Some(_) should only mean the internal channel exists; it must not authorize exposing public admin methods.

Proof of Concept

About PoC

  • The first test proves downstream unsafe-head adoption. It invokes the real AdminRpc handler, receives NetworkAdminQuery::PostUnsafePayload, passes the admin-posted payload into production InsertTask, configures the execution-layer mock to return Valid for new_payload and forkchoiceUpdated, and asserts that the node's unsafe_head becomes the attacker-controlled block.

admin_posted_payload_can_become_unsafe_head_when_el_returns_valid

  • The second test proves the sequencer confirmation-failure primitive end-to-end. It first uses the real AdminRpc handler, then production InsertTask, and then production PayloadBuilder. After the admin-posted payload becomes the unsafe head, the sequencer build path observes that attacker-controlled head. The selected L1 origin intentionally does not match either the unsafe head's L1 origin hash or parent hash. This follows the production guard inside PayloadBuilder::get_next_payload_l1_origin: the sequencer cannot build on an inconsistent L1 origin, so it calls reset_engine_forkchoice() and returns Ok(None). The test repeats this condition for three build attempts and asserts that start_build_block() is never called.

admin_payload_causes_sequencer_confirmation_failure_on_inconsistent_head

Run PoC

1

Paste the PoC code below into new file crates/consensus/service/tests/actors/admin_unsafe_payload_confirmation_failure.rs

2

In base/crates/consensus/service/tests/actors/mod.rs add this:

3

Run te POC with

Output PoC

PoC code

Was this helpful?