Root cause
SSRF & network boundaries introduced
fix(ui): parse gatewayUrl from URL params for remote gateway access
How AI contributed: Direct introductionPublished Feb 2, 2026 in
openclaw/openclawAll 35 findings in this repository
The Claude-coauthored Control UI read a gatewayUrl query parameter in applySettingsFromUrl and committed it to live settings, so opening a crafted link re-pointed the running app at a gateway of the attacker's choosing without any prompt. The UI then authenticated to that server with the stored gateway token, handing it to the attacker. The fix stores the proposed URL as pendingGatewayUrl and connects only after the user approves it in a confirmation dialog that shows the target.
applySettingsFromUrl applied the gatewayUrl parameter immediately and then cleaned it out of the address bar, so the app reconnected before the user could see or reject the change. The fix holds the value in pendingGatewayUrl and connects only after explicit confirmation.
Root cause
fix(ui): parse gatewayUrl from URL params for remote gateway access
Fix
The fix stops applying a gatewayUrl query parameter immediately, holds the proposed server URL in pending state, and connects only after the user explicitly confirms it in a warning dialog.
Fix by Tyler Yust · no AI marker found
Case facts
@@ -62,6 +62,7 @@ export function applySettingsFromUrl(host: SettingsHost) { const tokenRaw = params.get("token"); const passwordRaw = params.get("password"); const sessionRaw = params.get("session");+ const gatewayUrlRaw = params.get("gatewayUrl"); let shouldCleanUrl = false; if (tokenRaw != null) {@@ -94,6 +95,15 @@ export function applySettingsFromUrl(host: SettingsHost) { } } + if (gatewayUrlRaw != null) {+ const gatewayUrl = gatewayUrlRaw.trim();+ if (gatewayUrl && gatewayUrl !== host.settings.gatewayUrl) {+ applySettings(host, { ...host.settings, gatewayUrl });+ }+ params.delete("gatewayUrl");+ shouldCleanUrl = true;+ }+ if (!shouldCleanUrl) return; const url = new URL(window.location.href); url.search = params.toString();@@ -0,0 +1,39 @@+import { html, nothing } from "lit";++import type { AppViewState } from "../app-view-state";++export function renderGatewayUrlConfirmation(state: AppViewState) {+ const { pendingGatewayUrl } = state;+ if (!pendingGatewayUrl) return nothing;++ return html`+ <div class="exec-approval-overlay" role="dialog" aria-modal="true" aria-live="polite">+ <div class="exec-approval-card">+ <div class="exec-approval-header">+ <div>+ <div class="exec-approval-title">Change Gateway URL</div>+ <div class="exec-approval-sub">This will reconnect to a different gateway server</div>+ </div>+ </div>+ <div class="exec-approval-command mono">${pendingGatewayUrl}</div>+ <div class="callout danger" style="margin-top: 12px;">+ Only confirm if you trust this URL. Malicious URLs can compromise your system.+ </div>+ <div class="exec-approval-actions">+ <button+ class="btn primary"+ @click=${() => state.handleGatewayUrlConfirm()}+ >+ Confirm+ </button>+ <button+ class="btn"+ @click=${() => state.handleGatewayUrlCancel()}+ >+ Cancel+ </button>+ </div>+ </div>+ </div>+ `;+}@@ -33,6 +33,7 @@ type SettingsHost = { basePath: string; themeMedia: MediaQueryList | null; themeMediaHandler: ((event: MediaQueryListEvent) => void) | null;+ pendingGatewayUrl?: string | null; }; export function applySettings(host: SettingsHost, next: UiSettings) {@@ -98,7 +99,7 @@ export function applySettingsFromUrl(host: SettingsHost) { if (gatewayUrlRaw != null) { const gatewayUrl = gatewayUrlRaw.trim(); if (gatewayUrl && gatewayUrl !== host.settings.gatewayUrl) {- applySettings(host, { ...host.settings, gatewayUrl });+ host.pendingGatewayUrl = gatewayUrl; } params.delete("gatewayUrl"); shouldCleanUrl = true;@@ -152,6 +152,7 @@ export class MoltbotApp extends LitElement { @state() execApprovalQueue: ExecApprovalRequest[] = []; @state() execApprovalBusy = false; @state() execApprovalError: string | null = null;+ @state() pendingGatewayUrl: string | null = null; @state() configLoading = false; @state() configRaw = "{\n}\n";@@ -448,6 +449,21 @@ export class MoltbotApp extends LitElement { } } + handleGatewayUrlConfirm() {+ const nextGatewayUrl = this.pendingGatewayUrl;+ if (!nextGatewayUrl) return;+ this.pendingGatewayUrl = null;+ applySettingsInternal(+ this as unknown as Parameters<typeof applySettingsInternal>[0],+ { ...this.settings, gatewayUrl: nextGatewayUrl },+ );+ this.connect();+ }++ handleGatewayUrlCancel() {+ this.pendingGatewayUrl = null;+ }+ // Sidebar handlers for tool output viewing handleOpenSidebar(content: string) { if (this.sidebarCloseTimer != null) {AI-assisted change 30e2cdb89100c7ab8e61ef380c69da12b1bd66efc7695d23eac7f02fac151b79 · Fix c2e113850a9cb37152f942b826b73ae2cf9ffc03df54b24591a89a9809edc344