Safeguard
Application Security•

ASVS 5.0 V17.3.1: Rate Limit WebRTC Signaling Against Floods

ASVS V17.3.1 says the signaling server must keep serving legitimate messages during a flood, by rate limiting at the signaling level. Code and tests.

Safeguard Security Team
5 min read

ASVS 5.0 V17.3.1 requires that the signaling server keeps processing legitimate signaling messages during a flood attack, and says this should be achieved with rate limiting at the signaling level. It is a Level 2 requirement.

Signaling is the control channel for WebRTC. It carries offers, answers, ICE candidates and room events, usually over WebSocket or HTTPS. It is cheap to send to and expensive to process.

Why it matters

Each signaling message can trigger work: authentication, database lookups, room state changes and fan out to other participants. An attacker, or a bugged client in a loop, can saturate a server with a few thousand messages per second. Network level rate limits do not see inside a WebSocket frame, so the limit has to live in the signaling code.

How to implement it

Limit at several scopes: per connection, per authenticated user, per IP address, and globally. Use a token bucket per scope, differentiate by message type, and cut off abusers.

class Bucket {
  constructor(rate, burst) { this.rate = rate; this.burst = burst; this.tokens = burst; this.t = Date.now(); }
  take() {
    const now = Date.now();
    this.tokens = Math.min(this.burst, this.tokens + ((now - this.t) / 1000) * this.rate);
    this.t = now;
    if (this.tokens < 1) return false;
    this.tokens -= 1;
    return true;
  }
}

wss.on("connection", (ws, req) => {
  const perConn = new Bucket(20, 40);       // 20 msgs per second, burst 40
  let strikes = 0;
  ws.on("message", (data) => {
    if (data.length > 16 * 1024) return ws.close(1009, "too large");
    if (!perConn.take()) {
      if (++strikes > 50) return ws.close(1008, "rate limit");
      return;                                 // drop silently under the limit
    }
    handle(ws, data);
  });
});

Add these supporting controls:

  • Cap connections per IP and per user, and cap handshake attempts per minute.
  • Authenticate before heavy work, and keep unauthenticated message types minimal.
  • Limit ICE candidates and renegotiations per session.
  • Set a maximum frame size and an idle timeout.
  • Keep per connection state bounded, so floods do not grow memory.
  • Put a reverse proxy or gateway with connection limits in front for the upgrade request.

Use shared counters, for example a fast in memory store, if you run several signaling nodes, otherwise each node limits alone and an attacker spreads across them.

How to test it

  1. Open a normal session and record baseline message latency.
  2. From another client, send messages at 100 times the allowed rate. Confirm the flooder is throttled and then disconnected.
  3. While that runs, measure latency for the normal session. It should stay near baseline.
  4. Open hundreds of connections from one IP and confirm the cap triggers.
  5. Send an oversized frame and confirm it is refused.

A load tool such as k6 supports WebSocket scenarios and can script this.

Choosing numbers

Pick limits from data. Measure what a real client sends in a call: perhaps a join, an offer, an answer, and a few dozen ICE candidates in the first few seconds, then very little. A burst allowance that covers setup and a low sustained rate covers the rest. Set limits per message type, since candidate messages come in bursts while join and leave messages are rare and costly.

Tune with production metrics. Start by logging when the limit would trigger without enforcing it, review the near misses, and then enforce. This avoids cutting off real users during an unusual but legitimate burst, such as a large meeting where hundreds join in the same minute.

What to do on violation

Responses should escalate: drop the message, send a throttle notice, close the connection, then block the account or address temporarily. Record each step as a security event with the user, address and message type. Return a generic reason to the client, and add a retry hint with a delay so honest clients back off.

Layers of defense

Keep a limit at the edge for connection counts and handshake rates, and one in the application for messages. Combine them with authentication before expensive work, short idle timeouts, and bounded room sizes. In a multi tenant system add per tenant quotas so one customer cannot consume the shared pool. These layers matter because floods vary: some use many connections with few messages, others one connection with many.

Common mistakes

  • Rate limiting only the HTTP upgrade and not the messages that follow.
  • A limit per connection only, so an attacker opens thousands of connections.
  • Per node counters in a multi node deployment.
  • Disconnecting with an error that invites an instant reconnect storm. Add backoff guidance for clients.
  • Applying the same limit to cheap and expensive message types.

A worked scenario

A conferencing app with one signaling cluster sees a single account send ten thousand candidate messages in a few seconds from a looping client. Without limits, each message triggers a room lookup and a broadcast to forty participants, roughly four hundred thousand sends. With a per connection bucket of twenty messages per second, the account is cut to a few hundred operations, receives a throttle notice, and is disconnected after repeated violations. Other rooms never notice.

That difference between an incident and a log line is what the requirement is asking you to build.

Quick check

Why is a network firewall limit not enough for signaling?

It sees connections and bytes, not messages. A flood of small valid looking frames over one connection passes straight through.

What extra control is needed with several signaling nodes?

Shared counters, so the limit applies to the user or IP across all nodes.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.