Summary
The Arc consensus REST API exposes state-changing endpoints that allow any network client to modify the node’s persistent peer configuration:
POST /persistent-peers
DELETE /persistent-peers
The RPC router does not apply authentication or authorization middleware before these handlers are registered.
This becomes remotely exploitable whenever operators bind the RPC server to a non-loopback interface, which the Arc documentation explicitly demonstrates using:
An unauthenticated client that can reach the RPC port can therefore add attacker-controlled persistent peers or remove legitimate peers at runtime.
Repository
circlefin/arc-node
Create issue:
https://github.com/circlefin/arc-node/issues/new
Affected files
RPC routes
crates/malachite-app/src/rpc/routes.rs
|
"/network-state", |
|
crate::rpc::handlers::get_network_state, |
|
"Get the current network state (peers, topics, scores)" |
|
), |
|
route!( |
|
post, |
|
"/persistent-peers", |
|
crate::rpc::handlers::add_persistent_peer, |
|
"Add a persistent peer at runtime.", |
|
params = { |
|
"body" => "JSON object with \"addr\" (string): multiaddr of the peer, e.g. \"/ip4/127.0.0.1/tcp/26656/p2p/12D3KooW...\"." |
|
} |
|
), |
|
route!( |
|
delete, |
|
"/persistent-peers", |
|
crate::rpc::handlers::remove_persistent_peer, |
|
"Remove a persistent peer at runtime.", |
|
params = { |
|
"body" => "JSON object with \"addr\" (string): multiaddr of the peer to remove, e.g. \"/ip4/127.0.0.1/tcp/26656/p2p/12D3KooW...\"." |
|
} |
|
), |
|
]; |
|
|
|
#[tracing::instrument(name = "rpc", skip_all)] |
The mutating routes are registered as:
route!(
post,
"/persistent-peers",
crate::rpc::handlers::add_persistent_peer,
"Add a persistent peer at runtime."
),
route!(
delete,
"/persistent-peers",
crate::rpc::handlers::remove_persistent_peer,
"Remove a persistent peer at runtime."
),
Router construction
|
/// This is exposed publicly for testing purposes, allowing integration tests |
|
/// to create a server with the actual production router. |
|
pub fn build_router( |
|
tx_consensus_req: TxConsensusReq, |
|
tx_app_req: TxAppReq, |
|
tx_network_req: TxNetworkReq, |
|
) -> Router { |
|
let rpc_state = RpcState { |
|
tx_consensus_req, |
|
tx_app_req, |
|
tx_network_req, |
|
}; |
|
|
|
let routes = build_routes(); |
|
|
|
let mut router = Router::new(); |
|
for route in &routes { |
|
router = router.route(route.path, (route.handler)()); |
|
} |
|
|
|
let docs = routes |
|
.into_iter() |
|
.map(|r| (format!("{} {}", r.method, r.path), r.doc)) |
|
.collect::<BTreeMap<_, _>>(); |
|
|
|
router = { |
|
let docs = Arc::new(docs); |
|
router.route("/", get(move || get_index(Arc::clone(&docs)))) |
|
}; |
|
|
|
// Apply version extraction middleware |
|
router |
|
.layer(axum::middleware::from_fn(extract_version)) |
|
.with_state(rpc_state) |
|
} |
|
|
|
async fn inner( |
The router applies version extraction middleware, but no authentication or authorization layer:
router
.layer(axum::middleware::from_fn(extract_version))
.with_state(rpc_state)
Handlers
crates/malachite-app/src/rpc/handlers.rs
Add persistent peer:
|
Extension(version): Extension<ApiVersion>, |
|
Json(body): Json<AddOrRemovePersistentPeerBody>, |
|
) -> impl IntoResponse { |
|
let addr: malachitebft_app_channel::app::net::Multiaddr = match body.addr.parse() { |
|
Ok(a) => a, |
|
Err(_) => { |
|
let body = Json(json!({"error": "Invalid multiaddr"})); |
|
return (StatusCode::BAD_REQUEST, body).into_response(); |
|
} |
|
}; |
|
|
|
tracing::debug!(?version, ?addr, "add_persistent_peer called"); |
|
|
|
// For future ref: https://github.com/circlefin/malachite/pull/1485 |
|
// let has_p2p = addr.iter().any(|p| matches!(p, multiaddr::Protocol::P2p(_))); |
|
// if !has_p2p { |
|
// let body = Json(json!({ |
|
// "error": "Multiaddr must include /p2p/<peer_id>, e.g. /ip4/127.0.0.1/tcp/26656/p2p/12D3KooW..." |
|
// })); |
|
// return (StatusCode::BAD_REQUEST, body).into_response(); |
|
// } |
|
|
|
match NetworkRequest::add_persistent_peer(&tx_network_req, addr).await { |
|
Ok(Ok(())) => (StatusCode::OK, Json(json!({ "status": "ok" }))).into_response(), |
|
Ok(Err(e)) => persistent_peer_error_to_response(e).into_response(), |
|
Err(e) => request_error_to_response(e).into_response(), |
|
} |
|
} |
|
|
|
pub(crate) async fn remove_persistent_peer( |
Remove persistent peer:
https://github.com/circlefin/arc-node/blob/745ba4eac61a553560a77c8b384c75aa6342da3f/crates/malachite-app/src/rpc/handlers.rs#L237-L256
The handlers directly forward the supplied multiaddress to the networking subsystem:
NetworkRequest::add_persistent_peer(&tx_network_req, addr).await
and:
NetworkRequest::remove_persistent_peer(&tx_network_req, addr).await
No caller identity, token, source-address restriction, or operator permission is checked.
Documentation showing remote exposure
crates/malachite-app/README.md
|
```bash |
|
arc-node-consensus start \ |
|
--home=~/.arc/consensus \ |
|
--moniker=validator-1 \ |
|
--validator \ |
|
--suggested-fee-recipient=0xYourAddressHere \ |
|
--p2p.addr=/ip4/172.19.0.5/tcp/27000 \ |
|
--p2p.persistent-peers=/ip4/172.19.0.6/tcp/27000,/ip4/172.19.0.7/tcp/27000 \ |
|
--metrics=172.19.0.5:29000 \ |
|
--rpc.addr=0.0.0.0:31000 \ |
|
--eth-socket=/tmp/reth.ipc \ |
|
--execution-socket=/tmp/auth.ipc \ |
|
--minimal |
|
``` |
|
|
|
**Full example with RPC** (for remote deployments): |
|
|
|
```bash |
|
arc-node-consensus start \ |
|
--home=~/.arc/consensus \ |
|
--moniker=validator-1 \ |
|
--validator \ |
|
--suggested-fee-recipient=0xYourAddressHere \ |
|
--p2p.addr=/ip4/172.19.0.5/tcp/27000 \ |
|
--p2p.persistent-peers=/ip4/172.19.0.6/tcp/27000,/ip4/172.19.0.7/tcp/27000 \ |
|
--metrics=0.0.0.0:29000 \ |
|
--rpc.addr=0.0.0.0:31000 \ |
|
--eth-rpc-endpoint=http://localhost:8545 \ |
|
--execution-endpoint=http://localhost:8551 \ |
|
--execution-jwt=jwtsecret \ |
|
--minimal |
|
``` |
|
|
The documented validator configurations bind RPC to all interfaces:
The REST API documentation also describes binding the endpoint to all interfaces:
|
The consensus layer exposes a REST API for monitoring and querying consensus state when `--rpc.addr` is set (e.g., `--rpc.addr=0.0.0.0:26658`). |
|
|
|
### API Versioning |
|
|
Steps to reproduce
Start an Arc consensus node with an externally reachable RPC endpoint:
arc-node-consensus start \
--rpc.addr=0.0.0.0:31000 \
[other required arguments]
From another host, submit an unauthenticated request:
curl -X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/vnd.arc.v1+json' \
http://NODE_IP:31000/persistent-peers \
-d '{
"addr": "/ip4/ATTACKER_IP/tcp/27000/p2p/ATTACKER_PEER_ID"
}'
The request reaches NetworkRequest::add_persistent_peer() without credentials.
A legitimate persistent peer can likewise be removed:
curl -X DELETE \
-H 'Content-Type: application/json' \
-H 'Accept: application/vnd.arc.v1+json' \
http://NODE_IP:31000/persistent-peers \
-d '{
"addr": "/ip4/LEGITIMATE_PEER_IP/tcp/27000/p2p/LEGITIMATE_PEER_ID"
}'
Expected behavior
Runtime operations that modify the node’s networking configuration should be restricted to authorized operators.
Read-only monitoring endpoints may remain public, but peer-management endpoints should require authentication or should only be available through a separate administrative interface bound to localhost by default.
Actual behavior
Any client capable of reaching the RPC listener can invoke the persistent-peer mutation handlers.
The Accept header only selects the API version and does not authenticate the caller.
Impact
An attacker with network access to the RPC port may be able to:
- add attacker-controlled persistent peers;
- remove trusted persistent peers;
- repeatedly alter the node’s peer configuration;
- influence which nodes receive persistent connection attempts;
- degrade node connectivity;
- interfere with sentry-node topology;
- increase exposure to eclipse-style isolation attempts;
- trigger repeated outbound connection work;
- disrupt validators that depend on a restricted persistent-peer set.
The impact is greater for validators using:
--p2p.persistent-peers-only
because removing or replacing the configured persistent peers may directly affect the validator’s available network topology.
Suggested fix
Separate public monitoring RPC from privileged administrative RPC.
At minimum:
-
Require authentication for POST /persistent-peers and DELETE /persistent-peers.
-
Bind administrative endpoints to loopback by default.
-
Do not register mutating routes on the public REST listener unless explicitly enabled.
-
Add a dedicated configuration flag, for example:
--rpc.admin
--rpc.admin-addr=127.0.0.1:26659
--rpc.admin-token-file=/path/to/token
-
Support TLS or deployment behind an authenticated reverse proxy when remote administration is required.
-
Clearly document that the public RPC interface must not expose administrative routes.
A simple bearer-token middleware would be an improvement, but a separate admin listener would provide stronger isolation.
Tests
Add integration tests confirming that:
- unauthenticated
POST /persistent-peers is rejected;
- unauthenticated
DELETE /persistent-peers is rejected;
- read-only routes remain accessible as intended;
- valid administrative credentials permit peer updates;
- invalid or missing credentials return
401 or 403;
- administrative routes are not registered when admin RPC is disabled;
- the default administrative listener uses a loopback address.
Acceptance criteria
Summary
The Arc consensus REST API exposes state-changing endpoints that allow any network client to modify the node’s persistent peer configuration:
The RPC router does not apply authentication or authorization middleware before these handlers are registered.
This becomes remotely exploitable whenever operators bind the RPC server to a non-loopback interface, which the Arc documentation explicitly demonstrates using:
An unauthenticated client that can reach the RPC port can therefore add attacker-controlled persistent peers or remove legitimate peers at runtime.
Repository
circlefin/arc-nodeCreate issue:
https://github.com/circlefin/arc-node/issues/new
Affected files
RPC routes
crates/malachite-app/src/rpc/routes.rsarc-node/crates/malachite-app/src/rpc/routes.rs
Lines 103 to 127 in 745ba4e
The mutating routes are registered as:
Router construction
arc-node/crates/malachite-app/src/rpc/routes.rs
Lines 141 to 177 in 745ba4e
The router applies version extraction middleware, but no authentication or authorization layer:
router .layer(axum::middleware::from_fn(extract_version)) .with_state(rpc_state)Handlers
crates/malachite-app/src/rpc/handlers.rsAdd persistent peer:
arc-node/crates/malachite-app/src/rpc/handlers.rs
Lines 206 to 235 in 745ba4e
Remove persistent peer:
https://github.com/circlefin/arc-node/blob/745ba4eac61a553560a77c8b384c75aa6342da3f/crates/malachite-app/src/rpc/handlers.rs#L237-L256
The handlers directly forward the supplied multiaddress to the networking subsystem:
and:
No caller identity, token, source-address restriction, or operator permission is checked.
Documentation showing remote exposure
crates/malachite-app/README.mdarc-node/crates/malachite-app/README.md
Lines 72 to 104 in 745ba4e
The documented validator configurations bind RPC to all interfaces:
The REST API documentation also describes binding the endpoint to all interfaces:
arc-node/crates/malachite-app/README.md
Lines 277 to 280 in 745ba4e
Steps to reproduce
Start an Arc consensus node with an externally reachable RPC endpoint:
From another host, submit an unauthenticated request:
The request reaches
NetworkRequest::add_persistent_peer()without credentials.A legitimate persistent peer can likewise be removed:
Expected behavior
Runtime operations that modify the node’s networking configuration should be restricted to authorized operators.
Read-only monitoring endpoints may remain public, but peer-management endpoints should require authentication or should only be available through a separate administrative interface bound to localhost by default.
Actual behavior
Any client capable of reaching the RPC listener can invoke the persistent-peer mutation handlers.
The
Acceptheader only selects the API version and does not authenticate the caller.Impact
An attacker with network access to the RPC port may be able to:
The impact is greater for validators using:
because removing or replacing the configured persistent peers may directly affect the validator’s available network topology.
Suggested fix
Separate public monitoring RPC from privileged administrative RPC.
At minimum:
Require authentication for
POST /persistent-peersandDELETE /persistent-peers.Bind administrative endpoints to loopback by default.
Do not register mutating routes on the public REST listener unless explicitly enabled.
Add a dedicated configuration flag, for example:
Support TLS or deployment behind an authenticated reverse proxy when remote administration is required.
Clearly document that the public RPC interface must not expose administrative routes.
A simple bearer-token middleware would be an improvement, but a separate admin listener would provide stronger isolation.
Tests
Add integration tests confirming that:
POST /persistent-peersis rejected;DELETE /persistent-peersis rejected;401or403;Acceptance criteria