#42752 [BC-High] Signer can be DOSed through their libp2p component

Submitted on Mar 25th 2025 at 19:31:11 UTC by @leadwiz for Attackathon | Stacks II

  • Report ID: #42752

  • Report Type: Blockchain/DLT

  • Report severity: High

  • Target: https://github.com/stacks-network/sbtc/tree/immunefi_attackaton_1.0

  • Impacts:

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

Description

Brief/Intro

The Signer service in the protocol stack implements a libp2p-based peer-to-peer networking layer via SwarmBuilder. While this enables decentralized communication for signing duties (e.g., pubsub-based coordination), it also introduces a significant Denial of Service (DoS) vulnerability. Any external peer can initiate TCP/libp2p connections to the signer node and cause resource exhaustion — leading to degraded performance or full signer unavailability.

This issue arises from unprotected usage of the SwarmBuilder and lack of early-stage peer filtering or connection gating.

Vulnerability Details

The signer node initializes a libp2p swarm as follows:

    async fn run_libp2p_swarm(ctx: impl Context) -> Result<(), Error> {
        tracing::info!("initializing the p2p network");

        // Build the swarm.
        tracing::debug!("building the libp2p swarm");
        let config = ctx.config();

        let enable_quic = config.signer.p2p.is_quic_used();

        let mut swarm = SignerSwarmBuilder::new(&config.signer.private_key)
            .add_listen_endpoints(&ctx.config().signer.p2p.listen_on)
            .add_seed_addrs(&ctx.config().signer.p2p.seeds)
            .add_external_addresses(&ctx.config().signer.p2p.public_endpoints)
            .enable_mdns(config.signer.p2p.enable_mdns)
            .enable_quic_transport(enable_quic)
            .build()?;

        // Start the libp2p swarm. This will run until either the shutdown signal is
        // received, or an unrecoverable error has occurred.
        tracing::info!("starting the libp2p swarm");
        swarm
            .start(&ctx)
            .in_current_span()
            .await
            .map_err(Error::SignerSwarm)
    }

This opens a public port (typically /ip4/0.0.0.0/tcp/PORT/p2p/PEER_ID) which any peer can connect to. There are no restrictions at the connection layer (e.g., libp2p's ConnectionGater) or rate limits. As a result, the signer accepts and processes incoming libp2p connection attempts from any peer, which includes:

  • TCP connection establishment

  • libp2p handshake (identify, ping, etc.)

  • potential gossip/ping/autonat protocols

Even if peers are later rejected based on business logic (is_allowed_peer()), this happens after connection establishment, consuming CPU and memory.


Impact Details

This vulnerability enables an unauthenticated attacker to:

  • Initiate hundreds or thousands of connections per second to the signer node.

  • Consume system resources (CPU, memory, file descriptors).

  • Stall or disrupt signing operations due to swarm loop congestion or libp2p thread starvation.

  • Prevent authorized peers from connecting or propagating signing messages in time.

  • Potentially crash the signer due to resource exhaustion, or trigger OOM kills if not rate-limited by OS.

The signer is responsible for critical operations and availability is essential.

This risk is amplified if:

  • The signer is part of a threshold set (and quorum depends on its presence).

  • There is no auto-recovery or failover mechanism.


Recommendations

To mitigate this issue:

  1. Set ConnectionLimits on SwarmBuilder to cap incoming and total connections:

  2. Track and rate-limit connections by IP address using a lightweight in-memory structure.

References

DOS Mitigation

Proof of Concept

Proof of Concept

  1. Change docker/docker-compose.yml so the p2p port is exposed.

  1. Run make devenv-up

  2. Replace the in the script below and run the script, while monitoring the CPU consumption.

Was this helpful?