Skip to content

fix(voice-call): reject oversized realtime WebSocket frames to prevent gateway DoS [AI-assisted]#63890

Merged
steipete merged 6 commits into
openclaw:mainfrom
mmaps:fix/fix-387
Apr 10, 2026
Merged

fix(voice-call): reject oversized realtime WebSocket frames to prevent gateway DoS [AI-assisted]#63890
steipete merged 6 commits into
openclaw:mainfrom
mmaps:fix/fix-387

Conversation

@mmaps

@mmaps mmaps commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: The voice-call realtime WebSocket endpoint (RealtimeCallHandler.handleWebSocketUpgrade) created a WebSocketServer with no maxPayload limit and no error event handler. Sending an oversized frame caused the ws runtime to throw an uncaught RangeError, crashing the entire gateway process.
  • Why it matters: Any caller who can reach the realtime stream URL (valid token required) could crash the gateway, dropping all active sessions across all channels.
  • What changed: Added maxPayload: 256 KB to the per-connection WebSocketServer so the ws library rejects oversized frames with a 1009 close code before JSON parsing or bridge setup runs. Added a ws.on("error", ...) handler to absorb the resulting error event. Refactored ActiveRealtimeVoiceBridge as Pick<RealtimeVoiceBridge, ...> to keep the local type anchored to the SDK definition.
  • What did NOT change: No changes to auth, routing, config schema, the bridge protocol, or any other channel.

Change Type (select all)

  • Bug fix
  • Security hardening

Scope (select all touched areas)

  • Integrations

Linked Issue/PR

  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: WebSocketServer was constructed without maxPayload, so the ws library had no frame-size bound. On receipt of an oversized frame it emits an error event; with no handler registered, Node.js treats it as an uncaught exception and terminates the process.
  • Missing detection / guardrail: No frame-size limit and no error event handler on the per-connection WebSocket instance.
  • Contributing context (if known): The gateway runs voice-call in-process, so a single crashed WebSocket tears down the whole gateway.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Seam / integration test
  • Target test or file: extensions/voice-call/src/webhook/realtime-handler.test.ts
  • Scenario the test should lock in: A WebSocket client that sends a >256 KB frame receives a 1009 close code, and createBridge / processEvent / getCallByProviderCallId are never called.
  • Why this is the smallest reliable guardrail: Exercises the real ws maxPayload enforcement path end-to-end without requiring a live Twilio session.
  • Existing test that already covers this (if any): None — new test added.

User-visible / Behavior Changes

Realtime stream clients that send frames larger than 256 KB will now receive a WebSocket 1009 (Message Too Big) close frame instead of crashing the gateway. Normal Twilio Media Stream control frames are well under this limit.

Diagram (if applicable)

Before:
[oversized frame] -> ws runtime throws RangeError -> uncaught exception -> gateway crash

After:
[oversized frame] -> ws maxPayload check -> 1009 close sent -> error event logged -> session closed cleanly

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Linux
  • Runtime/container: Node 22, gateway in-process
  • Integration/channel: voice-call realtime WebSocket

Steps

  1. Start gateway with voice-call realtime enabled
  2. Obtain a valid stream token via the /voice/webhook TwiML endpoint
  3. Connect a WebSocket client and send a frame > 256 KB

Expected

  • Client receives close code 1009
  • Gateway continues running; all other sessions unaffected

Actual (before fix)

  • Gateway process crashes with RangeError: Max payload size exceeded

Evidence

  • Failing test/log before + passing after — new integration test RealtimeCallHandler websocket hardening > rejects oversized pre-start frames before bridge setup added and passing.

Human Verification (required)

  • Verified scenarios: Integration test runs end-to-end against a real HTTP/WebSocket server; asserts 1009 close code and confirms bridge/manager mocks are never invoked.
  • Edge cases checked: Frame sent before start event (pre-bridge); hasBridge flag correctly prevents access to uninitialized bridge on close.
  • What you did not verify: Live Twilio session with real media frames; Windows/macOS gateway restart behavior.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: 256 KB limit could reject unexpectedly large but legitimate frames in future protocol extensions.
    • Mitigation: Limit is 4× the largest documented Twilio Media Stream frame. If a legitimate use case requires larger frames, MAX_REALTIME_MESSAGE_BYTES is a named constant easy to adjust with a test update.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 275d99e738

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/voice-call/src/webhook/realtime-handler.ts
@greptile-apps

greptile-apps Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a real gateway DoS by adding maxPayload: 256 KB to the per-connection WebSocketServer and attaching a ws.on("error", …) handler so the resulting error event is absorbed instead of crashing Node.js. The ActiveRealtimeVoiceBridge type narrowing and hasBridge flag are clean improvements, and the new integration test exercises the real ws enforcement path end-to-end.

Confidence Score: 5/5

Safe to merge — the security fix is correct and well-tested; remaining findings are P2 style suggestions only.

The core change (adding maxPayload + error handler) is correct and directly addresses the DoS. The new integration test exercises the real enforcement path end-to-end. All remaining findings are P2: a TypeScript style preference (bridge! vs. nullable) and a minor test teardown gap that does not affect correctness.

No files require special attention before merging.

Vulnerabilities

  • DoS fix (positive): maxPayload: 256 * 1024 on the per-connection WebSocketServer prevents an authenticated caller from crashing the gateway with an oversized frame; confirmed by the new integration test.
  • No new permissions, secrets handling, or network surface changes were introduced.
  • No other security concerns identified.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/voice-call/src/webhook/realtime-handler.ts
Line: 144-145

Comment:
**Definite-assignment assertion weakens TypeScript null-safety**

`bridge!` tells TypeScript "this is always initialized," so it won't warn if future code accesses `bridge` without first checking `hasBridge`. The pre-existing pattern (`let bridge: ActiveRealtimeVoiceBridge | null = null`) kept the compiler on your side: every access had to be guarded or TypeScript would error. Consider reverting to the nullable form, which lets TypeScript enforce the guard rather than relying solely on the runtime `hasBridge` flag.

```suggestion
      let bridge: ActiveRealtimeVoiceBridge | null = null;
      let hasBridge = false;
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/voice-call/src/webhook/realtime-handler.test.ts
Line: 193-214

Comment:
**`server.close()` may stall if client WebSocket is still open**

The `finally` block closes the HTTP server but not the client `ws` instance. If an assertion throws before `waitForClose(ws)` returns (e.g., `connectWs` times out or an `expect` fails), `ws` is still open and `server.close()` won't resolve until Node drains it — which can make the test runner hang. Closing `ws` explicitly in `finally` avoids the stall.

```suggestion
    try {
      const ws = await connectWs(server.url);
      try {
        ws.send(
          JSON.stringify({
            event: "start",
            start: {
              streamSid: "MZ-oversized",
              callSid: "CA-oversized",
              padding: "A".repeat(300 * 1024),
            },
          }),
        );

        const closed = await waitForClose(ws);

        expect(closed.code).toBe(1009);
        expect(createBridge).not.toHaveBeenCalled();
        expect(processEvent).not.toHaveBeenCalled();
        expect(getCallByProviderCallId).not.toHaveBeenCalled();
      } finally {
        if (ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {
          ws.close();
        }
      }
    } finally {
      await server.close();
    }
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix: address review feedback" | Re-trigger Greptile

Comment thread extensions/voice-call/src/webhook/realtime-handler.ts Outdated
Comment thread extensions/voice-call/src/webhook/realtime-handler.test.ts
@openclaw-barnacle openclaw-barnacle Bot added channel: voice-call Channel integration: voice-call size: S labels Apr 9, 2026
@steipete
steipete merged commit afadb7d into openclaw:main Apr 10, 2026
7 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Landed via rebase onto main and squash merge.

  • Gate: pnpm test extensions/voice-call/src/webhook/realtime-handler.test.ts; pnpm check
  • Source head: 61f370f
  • Merge commit: afadb7d

Thanks @mmaps!

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

Labels

channel: voice-call Channel integration: voice-call size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants